Merge "Rename: ReadDmaBufs -> ReadProcfsDmaBufs"
diff --git a/core/api/current.txt b/core/api/current.txt
index 7acd940..c9b5af5 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -21304,7 +21304,7 @@
     field public static final int ERROR_RECLAIMED = 1101; // 0x44d
   }
 
-  public static final class MediaCodec.CryptoException extends java.lang.RuntimeException {
+  public static final class MediaCodec.CryptoException extends java.lang.RuntimeException implements android.media.MediaDrmThrowable {
     ctor public MediaCodec.CryptoException(int, @Nullable String);
     method public int getErrorCode();
     field @Deprecated public static final int ERROR_FRAME_TOO_LARGE = 8; // 0x8
@@ -21804,7 +21804,7 @@
     method public void setMediaDrmSession(@NonNull byte[]) throws android.media.MediaCryptoException;
   }
 
-  public final class MediaCryptoException extends java.lang.Exception {
+  public final class MediaCryptoException extends java.lang.Exception implements android.media.MediaDrmThrowable {
     ctor public MediaCryptoException(@Nullable String);
   }
 
@@ -22027,7 +22027,7 @@
     method public long getTimestampMillis();
   }
 
-  public static final class MediaDrm.MediaDrmStateException extends java.lang.IllegalStateException {
+  public static final class MediaDrm.MediaDrmStateException extends java.lang.IllegalStateException implements android.media.MediaDrmThrowable {
     method @NonNull public String getDiagnosticInfo();
     method public int getErrorCode();
     method public boolean isTransient();
@@ -22100,7 +22100,7 @@
   @Deprecated @IntDef({android.media.MediaDrm.SECURITY_LEVEL_UNKNOWN, android.media.MediaDrm.SECURITY_LEVEL_SW_SECURE_CRYPTO, android.media.MediaDrm.SECURITY_LEVEL_SW_SECURE_DECODE, android.media.MediaDrm.SECURITY_LEVEL_HW_SECURE_CRYPTO, android.media.MediaDrm.SECURITY_LEVEL_HW_SECURE_DECODE, android.media.MediaDrm.SECURITY_LEVEL_HW_SECURE_ALL}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface MediaDrm.SecurityLevel {
   }
 
-  public static final class MediaDrm.SessionException extends java.lang.RuntimeException {
+  public static final class MediaDrm.SessionException extends java.lang.RuntimeException implements android.media.MediaDrmThrowable {
     ctor public MediaDrm.SessionException(int, @Nullable String);
     method @Deprecated public int getErrorCode();
     method public boolean isTransient();
@@ -22108,14 +22108,20 @@
     field @Deprecated public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  public class MediaDrmException extends java.lang.Exception {
+  public class MediaDrmException extends java.lang.Exception implements android.media.MediaDrmThrowable {
     ctor public MediaDrmException(String);
   }
 
-  public class MediaDrmResetException extends java.lang.IllegalStateException {
+  public class MediaDrmResetException extends java.lang.IllegalStateException implements android.media.MediaDrmThrowable {
     ctor public MediaDrmResetException(String);
   }
 
+  public interface MediaDrmThrowable {
+    method public default int getErrorContext();
+    method public default int getOemError();
+    method public default int getVendorError();
+  }
+
   public final class MediaExtractor {
     ctor public MediaExtractor();
     method public boolean advance();
@@ -41831,6 +41837,9 @@
     field public static final int AUTHENTICATION_METHOD_CERT = 1; // 0x1
     field public static final int AUTHENTICATION_METHOD_EAP_ONLY = 0; // 0x0
     field public static final int EPDG_ADDRESS_CELLULAR_LOC = 3; // 0x3
+    field public static final int EPDG_ADDRESS_IPV4_ONLY = 2; // 0x2
+    field public static final int EPDG_ADDRESS_IPV4_PREFERRED = 0; // 0x0
+    field public static final int EPDG_ADDRESS_IPV6_PREFERRED = 1; // 0x1
     field public static final int EPDG_ADDRESS_PCO = 2; // 0x2
     field public static final int EPDG_ADDRESS_PLMN = 1; // 0x1
     field public static final int EPDG_ADDRESS_STATIC = 0; // 0x0
@@ -41845,6 +41854,7 @@
     field public static final String KEY_CHILD_SESSION_AES_CTR_KEY_SIZE_INT_ARRAY = "iwlan.child_session_aes_ctr_key_size_int_array";
     field public static final String KEY_DIFFIE_HELLMAN_GROUPS_INT_ARRAY = "iwlan.diffie_hellman_groups_int_array";
     field public static final String KEY_DPD_TIMER_SEC_INT = "iwlan.dpd_timer_sec_int";
+    field public static final String KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT = "iwlan.epdg_address_ip_type_preference_int";
     field public static final String KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY = "iwlan.epdg_address_priority_int_array";
     field public static final String KEY_EPDG_AUTHENTICATION_METHOD_INT = "iwlan.epdg_authentication_method_int";
     field public static final String KEY_EPDG_PCO_ID_IPV4_INT = "iwlan.epdg_pco_id_ipv4_int";
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index ed2c8eb..8d53959 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -446,6 +446,10 @@
 
 package android.telephony {
 
+  public class CarrierConfigManager {
+    field public static final String KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT = "min_udp_port_4500_nat_timeout_sec_int";
+  }
+
   public abstract class CellSignalStrength {
     method public static int getNumSignalStrengthLevels();
   }
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index f494fa6..db1db91 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -1019,9 +1019,10 @@
         registerService(Context.APPWIDGET_SERVICE, AppWidgetManager.class,
                 new CachedServiceFetcher<AppWidgetManager>() {
             @Override
-            public AppWidgetManager createService(ContextImpl ctx) throws ServiceNotFoundException {
-                IBinder b = ServiceManager.getServiceOrThrow(Context.APPWIDGET_SERVICE);
-                return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b));
+            public AppWidgetManager createService(ContextImpl ctx) {
+                IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
+                return b == null ? null : new AppWidgetManager(ctx,
+                        IAppWidgetService.Stub.asInterface(b));
             }});
 
         registerService(Context.MIDI_SERVICE, MidiManager.class,
diff --git a/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java b/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java
index c3dba33..38b3174 100644
--- a/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java
+++ b/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java
@@ -353,6 +353,11 @@
         return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_RCS);
     }
 
+    /** @hide */
+    public Map<Integer, Integer> getCapabilitiesMatchCriteria() {
+        return Collections.unmodifiableMap(new HashMap<>(mCapabilitiesMatchCriteria));
+    }
+
     @Override
     public int hashCode() {
         return Objects.hash(
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 3a7aea5..70cf973 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -114,13 +114,28 @@
     public static final String VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY =
             "vcn_restricted_transports";
 
+    /**
+     * Key for maximum number of parallel SAs for tunnel aggregation
+     *
+     * <p>If set to a value > 1, multiple tunnels will be set up, and inbound traffic will be
+     * aggregated over the various tunnels.
+     *
+     * <p>Defaults to 1, unless overridden by carrier config
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY =
+            "vcn_tunnel_aggregation_sa_count_max";
+
     /** List of Carrier Config options to extract from Carrier Config bundles. @hide */
     @NonNull
     public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS =
             new String[] {
                 VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
                 VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
-                VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY
+                VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
+                VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
             };
 
     private static final Map<
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index 399f11b..c731a80 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -982,6 +982,43 @@
 
 
     /**
+     * Wait until a debugger attaches. As soon as a debugger attaches,
+     * suspend all Java threads and send VM_START (a.k.a VM_INIT)
+     * packet.
+     *
+     * @hide
+     */
+    public static void suspendAllAndSendVmStart() {
+        if (!VMDebug.isDebuggingEnabled()) {
+            return;
+        }
+
+        // if DDMS is listening, inform them of our plight
+        System.out.println("Sending WAIT chunk");
+        byte[] data = new byte[] { 0 };     // 0 == "waiting for debugger"
+        Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1);
+        DdmServer.sendChunk(waitChunk);
+
+        // We must wait until a debugger is connected (debug socket is
+        // open and at least one non-DDM JDWP packedt has been received.
+        // This guarantees that oj-libjdwp has been attached and that
+        // ART's default implementation of suspendAllAndSendVmStart has
+        // been replaced with an implementation that will suspendAll and
+        // send VM_START.
+        System.out.println("Waiting for debugger first packet");
+        while (!isDebuggerConnected()) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ie) {
+            }
+        }
+
+        System.out.println("Debug.suspendAllAndSentVmStart");
+        VMDebug.suspendAllAndSendVmStart();
+        System.out.println("Debug.suspendAllAndSendVmStart, resumed");
+    }
+
+    /**
      * Wait until a debugger attaches.  As soon as the debugger attaches,
      * this returns, so you will need to place a breakpoint after the
      * waitForDebugger() call if you want to start tracing immediately.
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 394927e..b210c46 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -292,8 +292,10 @@
      * If the service is not running, servicemanager will attempt to start it, and this function
      * will wait for it to be ready.
      *
-     * @return {@code null} if the service is not declared in the manifest, or if there are
-     * permission problems, or if there are fatal errors.
+     * @throws SecurityException if the process does not have the permissions to check
+     * isDeclared() for the service.
+     * @return {@code null} if the service is not declared in the manifest, or if there
+     * are fatal errors.
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
diff --git a/core/java/android/os/ThreadLocalWorkSource.java b/core/java/android/os/ThreadLocalWorkSource.java
index 894b1cc4..e9adb20 100644
--- a/core/java/android/os/ThreadLocalWorkSource.java
+++ b/core/java/android/os/ThreadLocalWorkSource.java
@@ -39,8 +39,8 @@
  */
 public final class ThreadLocalWorkSource {
     public static final int UID_NONE = Message.UID_NONE;
-    private static final ThreadLocal<Integer> sWorkSourceUid =
-            ThreadLocal.withInitial(() -> UID_NONE);
+    private static final ThreadLocal<int []> sWorkSourceUid =
+            ThreadLocal.withInitial(() -> new int[] {UID_NONE});
 
     /**
      * Returns the UID to blame for the code currently executed on this thread.
@@ -50,7 +50,7 @@
      * <p>It can also be set manually using {@link #setUid(int)}.
      */
     public static int getUid() {
-        return sWorkSourceUid.get();
+        return sWorkSourceUid.get()[0];
     }
 
     /**
@@ -65,7 +65,7 @@
      */
     public static long setUid(int uid) {
         final long token = getToken();
-        sWorkSourceUid.set(uid);
+        sWorkSourceUid.get()[0] = uid;
         return token;
     }
 
@@ -73,7 +73,7 @@
      * Restores the state using the provided token.
      */
     public static void restore(long token) {
-        sWorkSourceUid.set(parseUidFromToken(token));
+        sWorkSourceUid.get()[0] = parseUidFromToken(token);
     }
 
     /**
@@ -88,7 +88,7 @@
      * </pre>
      *
      * @return a token that can be used to restore the state.
-     **/
+     */
     public static long clear() {
         return setUid(UID_NONE);
     }
@@ -98,7 +98,7 @@
     }
 
     private static long getToken() {
-        return sWorkSourceUid.get();
+        return sWorkSourceUid.get()[0];
     }
 
     private ThreadLocalWorkSource() {
diff --git a/core/java/android/security/rkp/IRemoteProvisioning.aidl b/core/java/android/security/rkp/IRemoteProvisioning.aidl
index 23d8159..0efaa89 100644
--- a/core/java/android/security/rkp/IRemoteProvisioning.aidl
+++ b/core/java/android/security/rkp/IRemoteProvisioning.aidl
@@ -58,11 +58,4 @@
      *
      */
     void getRegistration(String irpcName, IGetRegistrationCallback callback);
-
-    /**
-     * Cancel any active {@link getRegistration} call associated with the given
-     * callback. If no getRegistration call is currently active, this function is
-     * a noop.
-     */
-    void cancelGetRegistration(IGetRegistrationCallback callback);
 }
diff --git a/core/java/android/service/controls/OWNERS b/core/java/android/service/controls/OWNERS
new file mode 100644
index 0000000..4bb78c7
--- /dev/null
+++ b/core/java/android/service/controls/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 802726
+asc@google.com
+kozynski@google.com
+juliacr@google.com
\ No newline at end of file
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
index a24c1f9..492c938 100644
--- a/core/java/android/view/InputEventReceiver.java
+++ b/core/java/android/view/InputEventReceiver.java
@@ -77,7 +77,7 @@
         mInputChannel = inputChannel;
         mMessageQueue = looper.getQueue();
         mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
-                inputChannel, mMessageQueue);
+                mInputChannel, mMessageQueue);
 
         mCloseGuard.open("InputEventReceiver.dispose");
     }
diff --git a/core/java/android/view/InputEventSender.java b/core/java/android/view/InputEventSender.java
index 9035f3f..64f62c7 100644
--- a/core/java/android/view/InputEventSender.java
+++ b/core/java/android/view/InputEventSender.java
@@ -65,7 +65,7 @@
         mInputChannel = inputChannel;
         mMessageQueue = looper.getQueue();
         mSenderPtr = nativeInit(new WeakReference<InputEventSender>(this),
-                inputChannel, mMessageQueue);
+                mInputChannel, mMessageQueue);
 
         mCloseGuard.open("InputEventSender.dispose");
     }
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 949f363..7dd46a6 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -640,7 +640,6 @@
     char jitmaxsizeOptsBuf[sizeof("-Xjitmaxsize:")-1 + PROPERTY_VALUE_MAX];
     char jitinitialsizeOptsBuf[sizeof("-Xjitinitialsize:")-1 + PROPERTY_VALUE_MAX];
     char jitthresholdOptsBuf[sizeof("-Xjitthreshold:")-1 + PROPERTY_VALUE_MAX];
-    char useJitProfilesOptsBuf[sizeof("-Xjitsaveprofilinginfo:")-1 + PROPERTY_VALUE_MAX];
     char jitprithreadweightOptBuf[sizeof("-Xjitprithreadweight:")-1 + PROPERTY_VALUE_MAX];
     char jittransitionweightOptBuf[sizeof("-Xjittransitionweight:")-1 + PROPERTY_VALUE_MAX];
     char hotstartupsamplesOptsBuf[sizeof("-Xps-hot-startup-method-samples:")-1 + PROPERTY_VALUE_MAX];
@@ -856,10 +855,7 @@
     parseRuntimeOption("dalvik.vm.jitpthreadpriority",
                        jitpthreadpriorityOptsBuf,
                        "-Xjitpthreadpriority:");
-    property_get("dalvik.vm.usejitprofiles", useJitProfilesOptsBuf, "");
-    if (strcmp(useJitProfilesOptsBuf, "true") == 0) {
-        addOption("-Xjitsaveprofilinginfo");
-    }
+    addOption("-Xjitsaveprofilinginfo");
 
     parseRuntimeOption("dalvik.vm.jitprithreadweight",
                        jitprithreadweightOptBuf,
diff --git a/core/tests/coretests/src/android/service/controls/OWNERS b/core/tests/coretests/src/android/service/controls/OWNERS
new file mode 100644
index 0000000..bf67034
--- /dev/null
+++ b/core/tests/coretests/src/android/service/controls/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/core/java/android/service/controls/OWNERS
\ No newline at end of file
diff --git a/media/java/android/media/DeniedByServerException.java b/media/java/android/media/DeniedByServerException.java
index 9c1633a..98903ec 100644
--- a/media/java/android/media/DeniedByServerException.java
+++ b/media/java/android/media/DeniedByServerException.java
@@ -24,4 +24,11 @@
     public DeniedByServerException(String detailMessage) {
         super(detailMessage);
     }
+
+    /**
+     * @hide
+     */
+    public DeniedByServerException(String message, int vendorError, int oemError, int context) {
+        super(message, vendorError, oemError, context);
+    }
 }
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 220232d..8e8ed6c 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -2472,10 +2472,22 @@
     /**
      * Thrown when a crypto error occurs while queueing a secure input buffer.
      */
-    public final static class CryptoException extends RuntimeException {
+    public final static class CryptoException extends RuntimeException
+            implements MediaDrmThrowable {
         public CryptoException(int errorCode, @Nullable String detailMessage) {
-            super(detailMessage);
+            this(detailMessage, errorCode, 0, 0, 0);
+        }
+
+        /**
+         * @hide
+         */
+        public CryptoException(String message, int errorCode, int vendorError, int oemError,
+                int errorContext) {
+            super(message);
             mErrorCode = errorCode;
+            mVendorError = vendorError;
+            mOemError = oemError;
+            mErrorContext = errorContext;
         }
 
         /**
@@ -2594,7 +2606,22 @@
             return mErrorCode;
         }
 
-        private int mErrorCode;
+        @Override
+        public int getVendorError() {
+            return mVendorError;
+        }
+
+        @Override
+        public int getOemError() {
+            return mOemError;
+        }
+
+        @Override
+        public int getErrorContext() {
+            return mErrorContext;
+        }
+
+        private final int mErrorCode, mVendorError, mOemError, mErrorContext;
     }
 
     /**
diff --git a/media/java/android/media/MediaCryptoException.java b/media/java/android/media/MediaCryptoException.java
index 32ddf47..2a472fb 100644
--- a/media/java/android/media/MediaCryptoException.java
+++ b/media/java/android/media/MediaCryptoException.java
@@ -22,8 +22,35 @@
  * Exception thrown if MediaCrypto object could not be instantiated or
  * if unable to perform an operation on the MediaCrypto object.
  */
-public final class MediaCryptoException extends Exception {
+public final class MediaCryptoException extends Exception implements MediaDrmThrowable {
     public MediaCryptoException(@Nullable String detailMessage) {
-        super(detailMessage);
+        this(detailMessage, 0, 0, 0);
     }
+
+    /**
+     * @hide
+     */
+    public MediaCryptoException(String message, int vendorError, int oemError, int errorContext) {
+        super(message);
+        mVendorError = vendorError;
+        mOemError = oemError;
+        mErrorContext = errorContext;
+    }
+
+    @Override
+    public int getVendorError() {
+        return mVendorError;
+    }
+
+    @Override
+    public int getOemError() {
+        return mOemError;
+    }
+
+    @Override
+    public int getErrorContext() {
+        return mErrorContext;
+    }
+
+    private final int mVendorError, mOemError, mErrorContext;
 }
diff --git a/media/java/android/media/MediaDrm.java b/media/java/android/media/MediaDrm.java
index 2a04ebb..a3fa43d 100644
--- a/media/java/android/media/MediaDrm.java
+++ b/media/java/android/media/MediaDrm.java
@@ -664,21 +664,33 @@
      * strategy and details about each possible return value from {@link
      * MediaDrmStateException#getErrorCode()}.
      */
-    public static final class MediaDrmStateException extends java.lang.IllegalStateException {
-        private final int mErrorCode;
+    public static final class MediaDrmStateException extends java.lang.IllegalStateException
+            implements MediaDrmThrowable {
+        private final int mErrorCode, mVendorError, mOemError, mErrorContext;
         private final String mDiagnosticInfo;
 
         /**
          * @hide
          */
         public MediaDrmStateException(int errorCode, @Nullable String detailMessage) {
+            this(detailMessage, errorCode, 0, 0, 0);
+        }
+
+        /**
+         * @hide
+         */
+        public MediaDrmStateException(String detailMessage, int errorCode,
+                int vendorError, int oemError, int errorContext) {
             super(detailMessage);
             mErrorCode = errorCode;
+            mVendorError = vendorError;
+            mOemError = oemError;
+            mErrorContext = errorContext;
 
             // TODO get this from DRM session
             final String sign = errorCode < 0 ? "neg_" : "";
             mDiagnosticInfo =
-                "android.media.MediaDrm.error_" + sign + Math.abs(errorCode);
+                    "android.media.MediaDrm.error_" + sign + Math.abs(errorCode);
 
         }
 
@@ -696,6 +708,21 @@
             return mErrorCode;
         }
 
+        @Override
+        public int getVendorError() {
+            return mVendorError;
+        }
+
+        @Override
+        public int getOemError() {
+            return mOemError;
+        }
+
+        @Override
+        public int getErrorContext() {
+            return mErrorContext;
+        }
+
         /**
          * Returns true if the {@link MediaDrmStateException} is a transient
          * issue, perhaps due to resource constraints, and that the operation
@@ -727,10 +754,22 @@
      * {@link #isTransient()} to determine whether the app should retry the
      * failing operation.
      */
-    public static final class SessionException extends RuntimeException {
+    public static final class SessionException extends RuntimeException
+            implements MediaDrmThrowable {
         public SessionException(int errorCode, @Nullable String detailMessage) {
+            this(detailMessage, errorCode, 0, 0, 0);
+        }
+
+        /**
+         * @hide
+         */
+        public SessionException(String detailMessage, int errorCode, int vendorError, int oemError,
+                int errorContext) {
             super(detailMessage);
             mErrorCode = errorCode;
+            mVendorError = vendorError;
+            mOemError = oemError;
+            mErrorContext = errorContext;
         }
 
         /**
@@ -769,6 +808,21 @@
             return mErrorCode;
         }
 
+        @Override
+        public int getVendorError() {
+            return mVendorError;
+        }
+
+        @Override
+        public int getOemError() {
+            return mOemError;
+        }
+
+        @Override
+        public int getErrorContext() {
+            return mErrorContext;
+        }
+
         /**
          * Returns true if the {@link SessionException} is a transient
          * issue, perhaps due to resource constraints, and that the operation
@@ -779,7 +833,7 @@
             return mErrorCode == ERROR_RESOURCE_CONTENTION;
         }
 
-        private final int mErrorCode;
+        private final int mErrorCode, mVendorError, mOemError, mErrorContext;
     }
 
     /**
diff --git a/media/java/android/media/MediaDrmException.java b/media/java/android/media/MediaDrmException.java
index d547574..58c5dd0 100644
--- a/media/java/android/media/MediaDrmException.java
+++ b/media/java/android/media/MediaDrmException.java
@@ -19,8 +19,35 @@
 /**
  * Base class for MediaDrm exceptions
  */
-public class MediaDrmException extends Exception {
+public class MediaDrmException extends Exception implements MediaDrmThrowable {
     public MediaDrmException(String detailMessage) {
-        super(detailMessage);
+        this(detailMessage, 0, 0, 0);
     }
+
+    /**
+     * @hide
+     */
+    public MediaDrmException(String message, int vendorError, int oemError, int errorContext) {
+        super(message);
+        mVendorError = vendorError;
+        mOemError = oemError;
+        mErrorContext = errorContext;
+    }
+
+    @Override
+    public int getVendorError() {
+        return mVendorError;
+    }
+
+    @Override
+    public int getOemError() {
+        return mOemError;
+    }
+
+    @Override
+    public int getErrorContext() {
+        return mErrorContext;
+    }
+
+    private final int mVendorError, mOemError, mErrorContext;
 }
diff --git a/media/java/android/media/MediaDrmResetException.java b/media/java/android/media/MediaDrmResetException.java
index 3b2da1e..ccd723b 100644
--- a/media/java/android/media/MediaDrmResetException.java
+++ b/media/java/android/media/MediaDrmResetException.java
@@ -21,7 +21,7 @@
  * due to a restart of the mediaserver process.  To continue, the app must
  * release the MediaDrm object, then create and initialize a new one.
  */
-public class MediaDrmResetException extends IllegalStateException {
+public class MediaDrmResetException extends IllegalStateException implements MediaDrmThrowable {
     public MediaDrmResetException(String detailMessage) {
         super(detailMessage);
     }
diff --git a/media/java/android/media/MediaDrmThrowable.java b/media/java/android/media/MediaDrmThrowable.java
new file mode 100644
index 0000000..38480d7
--- /dev/null
+++ b/media/java/android/media/MediaDrmThrowable.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * A @{@link Throwable} thrown from {@link MediaDrm} or @{@link MediaCrypto} APIs
+ */
+public interface MediaDrmThrowable {
+    /**
+     * Returns {@link MediaDrm} plugin vendor defined error code associated with this {@link
+     * MediaDrmThrowable}.
+     * <p>
+     * Please consult the {@link MediaDrm} plugin vendor for details on the error code.
+     *
+     * @return an error code defined by the {@link MediaDrm} plugin vendor if available,
+     * otherwise 0.
+     */
+    public default int getVendorError() {
+        return 0;
+    }
+
+    /**
+     * Returns OEM or SOC specific error code associated with this {@link
+     * MediaDrmThrowable}.
+     * <p>
+     * Please consult the {@link MediaDrm} plugin, chip, or device vendor for details on the
+     * error code.
+     *
+     * @return an OEM or SOC specific error code if available, otherwise 0.
+     */
+    public default int getOemError() {
+        return 0;
+    }
+
+    /**
+     * Returns {@link MediaDrm} plugin vendor defined error context associated with this {@link
+     * MediaDrmThrowable}.
+     * <p>
+     * Please consult the {@link MediaDrm} plugin vendor for details on the error context.
+     *
+     * @return an opaque integer that would help the @{@link MediaDrm} vendor locate the
+     * source of the error if available, otherwise 0.
+     */
+    public default int getErrorContext() {
+        return 0;
+    }
+
+}
diff --git a/media/java/android/media/NotProvisionedException.java b/media/java/android/media/NotProvisionedException.java
index 32b8151..4b5a816 100644
--- a/media/java/android/media/NotProvisionedException.java
+++ b/media/java/android/media/NotProvisionedException.java
@@ -26,4 +26,11 @@
     public NotProvisionedException(String detailMessage) {
         super(detailMessage);
     }
+
+    /**
+     * @hide
+     */
+    public NotProvisionedException(String message, int vendorError, int oemError, int context) {
+        super(message, vendorError, oemError, context);
+    }
 }
diff --git a/media/java/android/media/ResourceBusyException.java b/media/java/android/media/ResourceBusyException.java
index a5abe21..7aaf7eb 100644
--- a/media/java/android/media/ResourceBusyException.java
+++ b/media/java/android/media/ResourceBusyException.java
@@ -24,4 +24,11 @@
     public ResourceBusyException(String detailMessage) {
         super(detailMessage);
     }
+
+    /**
+     * @hide
+     */
+    public ResourceBusyException(String message, int vendorError, int oemError, int context) {
+        super(message, vendorError, oemError, context);
+    }
 }
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 1183ca3..d8705a7 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -1285,7 +1285,7 @@
     CHECK(clazz.get() != NULL);
 
     jmethodID constructID =
-        env->GetMethodID(clazz.get(), "<init>", "(ILjava/lang/String;)V");
+        env->GetMethodID(clazz.get(), "<init>", "(Ljava/lang/String;IIII)V");
     CHECK(constructID != NULL);
 
     std::string defaultMsg = "Unknown Error";
@@ -1335,14 +1335,14 @@
             break;
     }
 
-    std::string msgStr(msg != NULL ? msg : defaultMsg.c_str());
-    if (crypto != NULL) {
-        msgStr = DrmUtils::GetExceptionMessage(err, msgStr.c_str(), crypto);
-    }
-    jstring msgObj = env->NewStringUTF(msgStr.c_str());
+    std::string originalMsg(msg != NULL ? msg : defaultMsg.c_str());
+    DrmStatus dStatus(err, originalMsg.c_str());
+    std::string detailedMsg(DrmUtils::GetExceptionMessage(dStatus, defaultMsg.c_str(), crypto));
+    jstring msgObj = env->NewStringUTF(detailedMsg.c_str());
 
     jthrowable exception =
-        (jthrowable)env->NewObject(clazz.get(), constructID, jerr, msgObj);
+        (jthrowable)env->NewObject(clazz.get(), constructID, msgObj, jerr,
+                                   dStatus.getCdmErr(), dStatus.getOemErr(), dStatus.getContext());
 
     env->Throw(exception);
 }
diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp
index c9d920f..681d76a 100644
--- a/media/jni/android_media_MediaDrm.cpp
+++ b/media/jni/android_media_MediaDrm.cpp
@@ -244,6 +244,20 @@
     }
     return arrayList;
 }
+
+int drmThrowException(JNIEnv* env, const char *className, const DrmStatus &err, const char *msg) {
+    using namespace android::jnihelp;
+    jstring _detailMessage = CreateExceptionMsg(env, msg);
+    int _status = ThrowException(env, className, "(Ljava/lang/String;III)V",
+                                 _detailMessage,
+                                 err.getCdmErr(),
+                                 err.getOemErr(),
+                                 err.getContext());
+    if (_detailMessage != NULL) {
+        env->DeleteLocalRef(_detailMessage);
+    }
+    return _status;
+}
 }  // namespace anonymous
 
 // ----------------------------------------------------------------------------
@@ -393,8 +407,8 @@
 
     jint jerr = MediaErrorToJavaError(err);
     jobject exception = env->NewObject(gFields.stateException.classId,
-            gFields.stateException.init, static_cast<int>(jerr),
-            env->NewStringUTF(msg));
+            gFields.stateException.init, env->NewStringUTF(msg), static_cast<int>(jerr),
+            err.getCdmErr(), err.getOemErr(), err.getContext());
     env->Throw(static_cast<jthrowable>(exception));
 }
 
@@ -411,10 +425,13 @@
     }
 
     jobject exception = env->NewObject(gFields.sessionException.classId,
-            gFields.sessionException.init, static_cast<int>(err),
-            env->NewStringUTF(msg));
+            gFields.sessionException.init,
+            env->NewStringUTF(msg),
+            jErrorCode,
+            err.getCdmErr(),
+            err.getOemErr(),
+            err.getContext());
 
-    env->SetIntField(exception, gFields.sessionException.errorCode, jErrorCode);
     env->Throw(static_cast<jthrowable>(exception));
 }
 
@@ -437,13 +454,13 @@
         jniThrowException(env, "java/lang/UnsupportedOperationException", msg);
         return true;
     } else if (err == ERROR_DRM_NOT_PROVISIONED) {
-        jniThrowException(env, "android/media/NotProvisionedException", msg);
+        drmThrowException(env, "android/media/NotProvisionedException", err, msg);
         return true;
     } else if (err == ERROR_DRM_RESOURCE_BUSY) {
-        jniThrowException(env, "android/media/ResourceBusyException", msg);
+        drmThrowException(env, "android/media/ResourceBusyException", err, msg);
         return true;
     } else if (err == ERROR_DRM_DEVICE_REVOKED) {
-        jniThrowException(env, "android/media/DeniedByServerException", msg);
+        drmThrowException(env, "android/media/DeniedByServerException", err, msg);
         return true;
     } else if (err == DEAD_OBJECT) {
         jniThrowException(env, "android/media/MediaDrmResetException", msg);
@@ -915,11 +932,11 @@
     gFields.arraylistClassId = static_cast<jclass>(env->NewGlobalRef(clazz));
 
     FIND_CLASS(clazz, "android/media/MediaDrm$MediaDrmStateException");
-    GET_METHOD_ID(gFields.stateException.init, clazz, "<init>", "(ILjava/lang/String;)V");
+    GET_METHOD_ID(gFields.stateException.init, clazz, "<init>", "(Ljava/lang/String;IIII)V");
     gFields.stateException.classId = static_cast<jclass>(env->NewGlobalRef(clazz));
 
     FIND_CLASS(clazz, "android/media/MediaDrm$SessionException");
-    GET_METHOD_ID(gFields.sessionException.init, clazz, "<init>", "(ILjava/lang/String;)V");
+    GET_METHOD_ID(gFields.sessionException.init, clazz, "<init>", "(Ljava/lang/String;IIII)V");
     gFields.sessionException.classId = static_cast<jclass>(env->NewGlobalRef(clazz));
     GET_FIELD_ID(gFields.sessionException.errorCode, clazz, "mErrorCode", "I");
 
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 430186a..6503029 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2797,7 +2797,11 @@
         try {
             pvr = verifyAndGetBypass(uid, packageName, null);
         } catch (SecurityException e) {
-            Slog.e(TAG, "Cannot setMode", e);
+            if (Process.isIsolated(uid)) {
+                Slog.e(TAG, "Cannot setMode: isolated process");
+            } else {
+                Slog.e(TAG, "Cannot setMode", e);
+            }
             return;
         }
 
@@ -3252,7 +3256,11 @@
         try {
             pvr = verifyAndGetBypass(uid, packageName, null);
         } catch (SecurityException e) {
-            Slog.e(TAG, "checkOperation", e);
+            if (Process.isIsolated(uid)) {
+                Slog.e(TAG, "Cannot checkOperation: isolated process");
+            } else {
+                Slog.e(TAG, "Cannot checkOperation", e);
+            }
             return AppOpsManager.opToDefaultMode(code);
         }
 
@@ -3458,7 +3466,11 @@
                 attributionTag = null;
             }
         } catch (SecurityException e) {
-            Slog.e(TAG, "noteOperation", e);
+            if (Process.isIsolated(uid)) {
+                Slog.e(TAG, "Cannot noteOperation: isolated process");
+            } else {
+                Slog.e(TAG, "Cannot noteOperation", e);
+            }
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
         }
@@ -3974,7 +3986,11 @@
                 attributionTag = null;
             }
         } catch (SecurityException e) {
-            Slog.e(TAG, "startOperation", e);
+            if (Process.isIsolated(uid)) {
+                Slog.e(TAG, "Cannot startOperation: isolated process");
+            } else {
+                Slog.e(TAG, "Cannot startOperation", e);
+            }
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
         }
@@ -4148,7 +4164,11 @@
                 attributionTag = null;
             }
         } catch (SecurityException e) {
-            Slog.e(TAG, "Cannot finishOperation", e);
+            if (Process.isIsolated(uid)) {
+                Slog.e(TAG, "Cannot finishOperation: isolated process");
+            } else {
+                Slog.e(TAG, "Cannot finishOperation", e);
+            }
             return;
         }
 
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 19dbee7..c15e419 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -140,6 +140,7 @@
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.server.DeviceIdleInternal;
@@ -2783,6 +2784,16 @@
         return hasIPV6 && !hasIPV4;
     }
 
+    private void setVpnNetworkPreference(String session, Set<Range<Integer>> ranges) {
+        BinderUtils.withCleanCallingIdentity(
+                () -> mConnectivityManager.setVpnDefaultForUids(session, ranges));
+    }
+
+    private void clearVpnNetworkPreference(String session) {
+        BinderUtils.withCleanCallingIdentity(
+                () -> mConnectivityManager.setVpnDefaultForUids(session, Collections.EMPTY_LIST));
+    }
+
     /**
      * Internal class managing IKEv2/IPsec VPN connectivity
      *
@@ -2894,6 +2905,9 @@
                     (r, exe) -> {
                         Log.d(TAG, "Runnable " + r + " rejected by the mExecutor");
                     });
+            setVpnNetworkPreference(mSessionKey,
+                    createUserAndRestrictedProfilesRanges(mUserId,
+                            mConfig.allowedApplications, mConfig.disallowedApplications));
         }
 
         @Override
@@ -3047,7 +3061,6 @@
                     mConfig.dnsServers.addAll(dnsAddrStrings);
 
                     mConfig.underlyingNetworks = new Network[] {network};
-                    mConfig.disallowedApplications = getAppExclusionList(mPackage);
 
                     networkAgent = mNetworkAgent;
 
@@ -3750,6 +3763,7 @@
             mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
             mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
                     mDiagnosticsCallback);
+            clearVpnNetworkPreference(mSessionKey);
 
             mExecutor.shutdown();
         }
@@ -4310,6 +4324,7 @@
             mConfig.requiresInternetValidation = profile.requiresInternetValidation;
             mConfig.excludeLocalRoutes = profile.excludeLocalRoutes;
             mConfig.allowBypass = profile.isBypassable;
+            mConfig.disallowedApplications = getAppExclusionList(mPackage);
 
             switch (profile.type) {
                 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
@@ -4462,6 +4477,9 @@
                         .setUids(createUserAndRestrictedProfilesRanges(
                                 mUserId, null /* allowedApplications */, excludedApps))
                         .build();
+                setVpnNetworkPreference(getSessionKeyLocked(),
+                        createUserAndRestrictedProfilesRanges(mUserId,
+                                mConfig.allowedApplications, mConfig.disallowedApplications));
                 doSendNetworkCapabilities(mNetworkAgent, mNetworkCapabilities);
             }
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 1e64701..89719ce 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -1772,7 +1772,7 @@
 
     private int runCompile() throws RemoteException {
         final PrintWriter pw = getOutPrintWriter();
-        boolean checkProfiles = SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false);
+        boolean checkProfiles = true;
         boolean forceCompilation = false;
         boolean allPackages = false;
         boolean clearProfileData = false;
diff --git a/services/core/java/com/android/server/pm/dex/ArtManagerService.java b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
index af507cd..50253ea 100644
--- a/services/core/java/com/android/server/pm/dex/ArtManagerService.java
+++ b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
@@ -320,15 +320,13 @@
 
         switch (profileType) {
             case ArtManager.PROFILE_APPS :
-                return SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false);
+                return true;
             case ArtManager.PROFILE_BOOT_IMAGE:
                 // The device config property overrides the system property version.
                 boolean profileBootClassPath = SystemProperties.getBoolean(
                         "persist.device_config.runtime_native_boot.profilebootclasspath",
                         SystemProperties.getBoolean("dalvik.vm.profilebootclasspath", false));
-                return (Build.IS_USERDEBUG || Build.IS_ENG) &&
-                        SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false) &&
-                        profileBootClassPath;
+                return (Build.IS_USERDEBUG || Build.IS_ENG) && profileBootClassPath;
             default:
                 throw new IllegalArgumentException("Invalid profile type:" + profileType);
         }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index dbf05f1..ed4ba0d 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -4212,8 +4212,8 @@
     }
 
     private boolean forceSuspendInternal(int uid) {
-        try {
-            synchronized (mLock) {
+        synchronized (mLock) {
+            try {
                 mForceSuspendActive = true;
                 // Place the system in an non-interactive state
                 for (int idx = 0; idx < mPowerGroups.size(); idx++) {
@@ -4223,16 +4223,14 @@
 
                 // Disable all the partial wake locks as well
                 updateWakeLockDisabledStatesLocked();
-            }
 
-            Slog.i(TAG, "Force-Suspending (uid " + uid + ")...");
-            boolean success = mNativeWrapper.nativeForceSuspend();
-            if (!success) {
-                Slog.i(TAG, "Force-Suspending failed in native.");
-            }
-            return success;
-        } finally {
-            synchronized (mLock) {
+                Slog.i(TAG, "Force-Suspending (uid " + uid + ")...");
+                boolean success = mNativeWrapper.nativeForceSuspend();
+                if (!success) {
+                    Slog.i(TAG, "Force-Suspending failed in native.");
+                }
+                return success;
+            } finally {
                 mForceSuspendActive = false;
                 // Re-enable wake locks once again.
                 updateWakeLockDisabledStatesLocked();
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
new file mode 100644
index 0000000..868f34b
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security.rkp;
+
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.os.OutcomeReceiver;
+import android.security.rkp.IGetKeyCallback;
+import android.security.rkp.IRegistration;
+import android.security.rkp.service.RegistrationProxy;
+import android.security.rkp.service.RemotelyProvisionedKey;
+import android.util.Log;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements android.security.rkp.IRegistration as a thin wrapper around the java code
+ * exported by com.android.rkp.
+ *
+ * @hide
+ */
+final class RemoteProvisioningRegistration extends IRegistration.Stub {
+    static final String TAG = RemoteProvisioningService.TAG;
+    private final ConcurrentHashMap<IGetKeyCallback, CancellationSignal> mOperations =
+            new ConcurrentHashMap<>();
+    private final RegistrationProxy mRegistration;
+    private final Executor mExecutor;
+
+    private class GetKeyReceiver implements OutcomeReceiver<RemotelyProvisionedKey, Exception> {
+        IGetKeyCallback mCallback;
+        GetKeyReceiver(IGetKeyCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onResult(RemotelyProvisionedKey result) {
+            mOperations.remove(mCallback);
+            Log.i(TAG, "Successfully fetched key for client " + mCallback.hashCode());
+            android.security.rkp.RemotelyProvisionedKey parcelable =
+                    new android.security.rkp.RemotelyProvisionedKey();
+            parcelable.keyBlob = result.getKeyBlob();
+            parcelable.encodedCertChain = result.getEncodedCertChain();
+            wrapCallback(() -> mCallback.onSuccess(parcelable));
+        }
+
+        @Override
+        public void onError(Exception e) {
+            mOperations.remove(mCallback);
+            if (e instanceof OperationCanceledException) {
+                Log.i(TAG, "Operation cancelled for client " + mCallback.hashCode());
+                wrapCallback(mCallback::onCancel);
+            } else {
+                Log.e(TAG, "Error fetching key for client " + mCallback.hashCode(), e);
+                wrapCallback(() -> mCallback.onError(e.getMessage()));
+            }
+        }
+    }
+
+    RemoteProvisioningRegistration(RegistrationProxy registration, Executor executor) {
+        mRegistration = registration;
+        mExecutor = executor;
+    }
+
+    @Override
+    public void getKey(int keyId, IGetKeyCallback callback) {
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        if (mOperations.putIfAbsent(callback, cancellationSignal) != null) {
+            Log.e(TAG, "Client can only request one call at a time " + callback.hashCode());
+            throw new IllegalArgumentException(
+                    "Callback is already associated with an existing operation: "
+                            + callback.hashCode());
+        }
+
+        try {
+            Log.i(TAG, "Fetching key " + keyId + " for client " + callback.hashCode());
+            mRegistration.getKeyAsync(keyId, cancellationSignal, mExecutor,
+                    new GetKeyReceiver(callback));
+        } catch (Exception e) {
+            Log.e(TAG, "getKeyAsync threw an exception for client " + callback.hashCode(), e);
+            mOperations.remove(callback);
+            wrapCallback(() -> callback.onError(e.getMessage()));
+        }
+    }
+
+    @Override
+    public void cancelGetKey(IGetKeyCallback callback) {
+        CancellationSignal cancellationSignal = mOperations.remove(callback);
+        if (cancellationSignal == null) {
+            throw new IllegalArgumentException(
+                    "Invalid client in cancelGetKey: " + callback.hashCode());
+        }
+
+        Log.i(TAG, "Requesting cancellation for client " + callback.hashCode());
+        cancellationSignal.cancel();
+    }
+
+    @Override
+    public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) {
+        // TODO(b/262748535)
+        Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED");
+    }
+
+    interface CallbackRunner {
+        void run() throws Exception;
+    }
+
+    private void wrapCallback(CallbackRunner callback) {
+        // Exceptions resulting from notifications to IGetKeyCallback objects can only be logged,
+        // since getKey execution is asynchronous, and there's no way for an exception to be
+        // properly handled up the stack.
+        try {
+            callback.run();
+        } catch (Exception e) {
+            Log.e(TAG, "Error invoking callback on client binder", e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
index 65a4b38..cd1a968 100644
--- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
@@ -20,9 +20,7 @@
 import android.os.Binder;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
-import android.security.rkp.IGetKeyCallback;
 import android.security.rkp.IGetRegistrationCallback;
-import android.security.rkp.IRegistration;
 import android.security.rkp.IRemoteProvisioning;
 import android.security.rkp.service.RegistrationProxy;
 import android.util.Log;
@@ -30,6 +28,7 @@
 import com.android.server.SystemService;
 
 import java.time.Duration;
+import java.util.concurrent.Executor;
 
 /**
  * Implements the remote provisioning system service. This service is backed by a mainline
@@ -43,6 +42,35 @@
     private static final Duration CREATE_REGISTRATION_TIMEOUT = Duration.ofSeconds(10);
     private final RemoteProvisioningImpl mBinderImpl = new RemoteProvisioningImpl();
 
+    private static class RegistrationReceiver implements
+            OutcomeReceiver<RegistrationProxy, Exception> {
+        private final Executor mExecutor;
+        private final IGetRegistrationCallback mCallback;
+
+        RegistrationReceiver(Executor executor, IGetRegistrationCallback callback) {
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onResult(RegistrationProxy registration) {
+            try {
+                mCallback.onSuccess(new RemoteProvisioningRegistration(registration, mExecutor));
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling success callback " + mCallback.hashCode(), e);
+            }
+        }
+
+        @Override
+        public void onError(Exception error) {
+            try {
+                mCallback.onError(error.toString());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling error callback " + mCallback.hashCode(), e);
+            }
+        }
+    }
+
     /** @hide */
     public RemoteProvisioningService(Context context) {
         super(context);
@@ -54,73 +82,20 @@
     }
 
     private final class RemoteProvisioningImpl extends IRemoteProvisioning.Stub {
-
-        final class RegistrationBinder extends IRegistration.Stub {
-            static final String TAG = RemoteProvisioningService.TAG;
-            private final RegistrationProxy mRegistration;
-
-            RegistrationBinder(RegistrationProxy registration) {
-                mRegistration = registration;
-            }
-
-            @Override
-            public void getKey(int keyId, IGetKeyCallback callback) {
-                Log.e(TAG, "RegistrationBinder.getKey NOT YET IMPLEMENTED");
-            }
-
-            @Override
-            public void cancelGetKey(IGetKeyCallback callback) {
-                Log.e(TAG, "RegistrationBinder.cancelGetKey NOT YET IMPLEMENTED");
-            }
-
-            @Override
-            public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) {
-                Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED");
-            }
-        }
-
         @Override
         public void getRegistration(String irpcName, IGetRegistrationCallback callback)
                 throws RemoteException {
             final int callerUid = Binder.getCallingUidOrThrow();
             final long callingIdentity = Binder.clearCallingIdentity();
+            final Executor executor = getContext().getMainExecutor();
             try {
                 Log.i(TAG, "getRegistration(" + irpcName + ")");
-                RegistrationProxy.createAsync(
-                        getContext(),
-                        callerUid,
-                        irpcName,
-                        CREATE_REGISTRATION_TIMEOUT,
-                        getContext().getMainExecutor(),
-                        new OutcomeReceiver<>() {
-                            @Override
-                            public void onResult(RegistrationProxy registration) {
-                                try {
-                                    callback.onSuccess(new RegistrationBinder(registration));
-                                } catch (RemoteException e) {
-                                    Log.e(TAG, "Error calling success callback", e);
-                                }
-                            }
-
-                            @Override
-                            public void onError(Exception error) {
-                                try {
-                                    callback.onError(error.toString());
-                                } catch (RemoteException e) {
-                                    Log.e(TAG, "Error calling error callback", e);
-                                }
-                            }
-                        });
+                RegistrationProxy.createAsync(getContext(), callerUid, irpcName,
+                        CREATE_REGISTRATION_TIMEOUT, executor,
+                        new RegistrationReceiver(executor, callback));
             } finally {
                 Binder.restoreCallingIdentity(callingIdentity);
             }
         }
-
-        @Override
-        public void cancelGetRegistration(IGetRegistrationCallback callback)
-                throws RemoteException {
-            Log.i(TAG, "cancelGetRegistration()");
-            callback.onError("cancelGetRegistration not yet implemented");
-        }
     }
 }
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 3be16a1..739aff7 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -33,6 +33,7 @@
 
 import static com.android.server.VcnManagementService.LOCAL_LOG;
 import static com.android.server.VcnManagementService.VDBG;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -59,6 +60,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.Uri;
 import android.net.annotations.PolicyDirection;
+import android.net.ipsec.ike.ChildSaProposal;
 import android.net.ipsec.ike.ChildSessionCallback;
 import android.net.ipsec.ike.ChildSessionConfiguration;
 import android.net.ipsec.ike.ChildSessionParams;
@@ -67,11 +69,14 @@
 import android.net.ipsec.ike.IkeSessionConfiguration;
 import android.net.ipsec.ike.IkeSessionConnectionInfo;
 import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.IkeTrafficSelector;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
 import android.net.ipsec.ike.exceptions.IkeException;
 import android.net.ipsec.ike.exceptions.IkeInternalException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
 import android.net.vcn.VcnGatewayConnectionConfig;
+import android.net.vcn.VcnManager;
 import android.net.vcn.VcnTransportInfo;
 import android.net.wifi.WifiInfo;
 import android.os.Handler;
@@ -169,6 +174,9 @@
 public class VcnGatewayConnection extends StateMachine {
     private static final String TAG = VcnGatewayConnection.class.getSimpleName();
 
+    /** Default number of parallel SAs requested */
+    static final int TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT = 1;
+
     // Matches DataConnection.NETWORK_TYPE private constant, and magic string from
     // ConnectivityManager#getNetworkTypeName()
     @VisibleForTesting(visibility = Visibility.PRIVATE)
@@ -1980,6 +1988,22 @@
                             mChildConfig,
                             oldChildConfig,
                             mIkeConnectionInfo);
+
+                    // Create opportunistic child SAs; this allows SA aggregation in the downlink,
+                    // reducing lock/atomic contention in high throughput scenarios. All SAs will
+                    // share the same UDP encap socket (and keepalives) as necessary, and are
+                    // effectively free.
+                    final int parallelTunnelCount =
+                            mDeps.getParallelTunnelCount(mLastSnapshot, mSubscriptionGroup);
+                    logInfo("Parallel tunnel count: " + parallelTunnelCount);
+
+                    for (int i = 0; i < parallelTunnelCount - 1; i++) {
+                        mIkeSession.openChildSession(
+                                buildOpportunisticChildParams(),
+                                new VcnChildSessionCallback(
+                                        mCurrentToken, true /* isOpportunistic */));
+                    }
+
                     break;
                 case EVENT_DISCONNECT_REQUESTED:
                     handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj);
@@ -2350,15 +2374,44 @@
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public class VcnChildSessionCallback implements ChildSessionCallback {
         private final int mToken;
+        private final boolean mIsOpportunistic;
+
+        private boolean mIsChildOpened = false;
 
         VcnChildSessionCallback(int token) {
+            this(token, false /* isOpportunistic */);
+        }
+
+        /**
+         * Creates a ChildSessionCallback
+         *
+         * <p>If configured as opportunistic, transforms will not report initial startup, or
+         * associated startup failures. This serves the dual purposes of ensuring that if the server
+         * does not support connection multiplexing, new child SA negotiations will be ignored, and
+         * at the same time, will notify the VCN session if a successfully negotiated opportunistic
+         * child SA is subsequently torn down, which could impact uplink traffic if the SA in use
+         * for outbound/uplink traffic is this opportunistic SA.
+         *
+         * <p>While inbound SAs can be used in parallel, the IPsec stack explicitly selects the last
+         * applied outbound transform for outbound traffic. This means that unlike inbound traffic,
+         * outbound does not benefit from these parallel SAs in the same manner.
+         */
+        VcnChildSessionCallback(int token, boolean isOpportunistic) {
             mToken = token;
+            mIsOpportunistic = isOpportunistic;
         }
 
         /** Internal proxy method for injecting of mocked ChildSessionConfiguration */
         @VisibleForTesting(visibility = Visibility.PRIVATE)
         void onOpened(@NonNull VcnChildSessionConfiguration childConfig) {
             logDbg("ChildOpened for token " + mToken);
+
+            if (mIsOpportunistic) {
+                logDbg("ChildOpened for opportunistic child; suppressing event message");
+                mIsChildOpened = true;
+                return;
+            }
+
             childOpened(mToken, childConfig);
         }
 
@@ -2370,12 +2423,24 @@
         @Override
         public void onClosed() {
             logDbg("ChildClosed for token " + mToken);
+
+            if (mIsOpportunistic && !mIsChildOpened) {
+                logDbg("ChildClosed for unopened opportunistic child; ignoring");
+                return;
+            }
+
             sessionLost(mToken, null);
         }
 
         @Override
         public void onClosedExceptionally(@NonNull IkeException exception) {
             logInfo("ChildClosedExceptionally for token " + mToken, exception);
+
+            if (mIsOpportunistic && !mIsChildOpened) {
+                logInfo("ChildClosedExceptionally for unopened opportunistic child; ignoring");
+                return;
+            }
+
             sessionLost(mToken, exception);
         }
 
@@ -2580,6 +2645,30 @@
         return mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams();
     }
 
+    private ChildSessionParams buildOpportunisticChildParams() {
+        final ChildSessionParams baseParams =
+                mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams();
+
+        final TunnelModeChildSessionParams.Builder builder =
+                new TunnelModeChildSessionParams.Builder();
+        for (ChildSaProposal proposal : baseParams.getChildSaProposals()) {
+            builder.addChildSaProposal(proposal);
+        }
+
+        for (IkeTrafficSelector inboundSelector : baseParams.getInboundTrafficSelectors()) {
+            builder.addInboundTrafficSelectors(inboundSelector);
+        }
+
+        for (IkeTrafficSelector outboundSelector : baseParams.getOutboundTrafficSelectors()) {
+            builder.addOutboundTrafficSelectors(outboundSelector);
+        }
+
+        builder.setLifetimeSeconds(
+                baseParams.getHardLifetimeSeconds(), baseParams.getSoftLifetimeSeconds());
+
+        return builder.build();
+    }
+
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     VcnIkeSession buildIkeSession(@NonNull Network network) {
         final int token = ++mCurrentToken;
@@ -2680,6 +2769,23 @@
                 return 0;
             }
         }
+
+        /** Gets the max number of parallel tunnels allowed for tunnel aggregation. */
+        public int getParallelTunnelCount(
+                TelephonySubscriptionSnapshot snapshot, ParcelUuid subGrp) {
+            PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp);
+            int result = TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT;
+
+            if (carrierConfig != null) {
+                result =
+                        carrierConfig.getInt(
+                                VcnManager.VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
+                                TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT);
+            }
+
+            // Guard against tunnel count < 1
+            return Math.max(1, result);
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
index 2f84fdd..2141eba 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.vcn.routeselection;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -45,6 +46,7 @@
 import com.android.server.vcn.VcnContext;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
@@ -69,9 +71,23 @@
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     static final int WIFI_EXIT_RSSI_THRESHOLD_DEFAULT = -74;
 
-    /** Priority for any other networks (including unvalidated, etc) */
+    /**
+     * Priority for networks that VCN can fall back to.
+     *
+     * <p>If none of the network candidates are validated or match any template, VCN will fall back
+     * to any INTERNET network.
+     */
     @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final int PRIORITY_ANY = Integer.MAX_VALUE;
+    static final int PRIORITY_FALLBACK = Integer.MAX_VALUE;
+
+    /**
+     * Priority for networks that cannot be selected as VCN's underlying networks.
+     *
+     * <p>VCN MUST never select a non-INTERNET network that are unvalidated or fail to match any
+     * template as the underlying network.
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PRIORITY_INVALID = -1;
 
     /** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */
     public static int calculatePriorityClass(
@@ -86,12 +102,12 @@
 
         if (networkRecord.isBlocked) {
             logWtf("Network blocked for System Server: " + networkRecord.network);
-            return PRIORITY_ANY;
+            return PRIORITY_INVALID;
         }
 
         if (snapshot == null) {
             logWtf("Got null snapshot");
-            return PRIORITY_ANY;
+            return PRIORITY_INVALID;
         }
 
         int priorityIndex = 0;
@@ -108,7 +124,13 @@
             }
             priorityIndex++;
         }
-        return PRIORITY_ANY;
+
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+        if (caps.hasCapability(NET_CAPABILITY_INTERNET)
+                || (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST))) {
+            return PRIORITY_FALLBACK;
+        }
+        return PRIORITY_INVALID;
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
@@ -297,6 +319,18 @@
             return false;
         }
 
+        for (Map.Entry<Integer, Integer> entry :
+                networkPriority.getCapabilitiesMatchCriteria().entrySet()) {
+            final int cap = entry.getKey();
+            final int matchCriteria = entry.getValue();
+
+            if (matchCriteria == MATCH_REQUIRED && !caps.hasCapability(cap)) {
+                return false;
+            } else if (matchCriteria == MATCH_FORBIDDEN && caps.hasCapability(cap)) {
+                return false;
+            }
+        }
+
         return true;
     }
 
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index d474c5d..6afa795 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -16,6 +16,9 @@
 
 package com.android.server.vcn.routeselection;
 
+import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_ANY;
+import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_FORBIDDEN;
+import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED;
 import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener;
 
 import static com.android.server.VcnManagementService.LOCAL_LOG;
@@ -32,6 +35,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnCellUnderlyingNetworkTemplate;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.os.Handler;
@@ -40,6 +44,7 @@
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -49,6 +54,7 @@
 import com.android.server.vcn.util.LogUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -126,6 +132,63 @@
         registerOrUpdateNetworkRequests();
     }
 
+    private static class CapabilityMatchCriteria {
+        public final int capability;
+        public final int matchCriteria;
+
+        CapabilityMatchCriteria(int capability, int matchCriteria) {
+            this.capability = capability;
+            this.matchCriteria = matchCriteria;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(capability, matchCriteria);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (!(other instanceof CapabilityMatchCriteria)) {
+                return false;
+            }
+
+            final CapabilityMatchCriteria rhs = (CapabilityMatchCriteria) other;
+            return capability == rhs.capability && matchCriteria == rhs.matchCriteria;
+        }
+    }
+
+    private static Set<Set<CapabilityMatchCriteria>> dedupAndGetCapRequirementsForCell(
+            VcnGatewayConnectionConfig connectionConfig) {
+        final Set<Set<CapabilityMatchCriteria>> dedupedCapsMatchSets = new ArraySet<>();
+
+        for (VcnUnderlyingNetworkTemplate template :
+                connectionConfig.getVcnUnderlyingNetworkPriorities()) {
+            if (template instanceof VcnCellUnderlyingNetworkTemplate) {
+                final Set<CapabilityMatchCriteria> capsMatchSet = new ArraySet<>();
+
+                for (Map.Entry<Integer, Integer> entry :
+                        ((VcnCellUnderlyingNetworkTemplate) template)
+                                .getCapabilitiesMatchCriteria()
+                                .entrySet()) {
+
+                    final int capability = entry.getKey();
+                    final int matchCriteria = entry.getValue();
+                    if (matchCriteria != MATCH_ANY) {
+                        capsMatchSet.add(new CapabilityMatchCriteria(capability, matchCriteria));
+                    }
+                }
+
+                dedupedCapsMatchSets.add(capsMatchSet);
+            }
+        }
+
+        dedupedCapsMatchSets.add(
+                Collections.singleton(
+                        new CapabilityMatchCriteria(
+                                NetworkCapabilities.NET_CAPABILITY_INTERNET, MATCH_REQUIRED)));
+        return dedupedCapsMatchSets;
+    }
+
     private void registerOrUpdateNetworkRequests() {
         NetworkCallback oldRouteSelectionCallback = mRouteSelectionCallback;
         NetworkCallback oldWifiCallback = mWifiBringupCallback;
@@ -158,11 +221,14 @@
                     getWifiNetworkRequest(), mWifiBringupCallback, mHandler);
 
             for (final int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) {
-                final NetworkBringupCallback cb = new NetworkBringupCallback();
-                mCellBringupCallbacks.add(cb);
+                for (Set<CapabilityMatchCriteria> capsMatchCriteria :
+                        dedupAndGetCapRequirementsForCell(mConnectionConfig)) {
+                    final NetworkBringupCallback cb = new NetworkBringupCallback();
+                    mCellBringupCallbacks.add(cb);
 
-                mConnectivityManager.requestBackgroundNetwork(
-                        getCellNetworkRequestForSubId(subId), cb, mHandler);
+                    mConnectivityManager.requestBackgroundNetwork(
+                            getCellNetworkRequestForSubId(subId, capsMatchCriteria), cb, mHandler);
+                }
             }
         } else {
             mRouteSelectionCallback = null;
@@ -214,6 +280,13 @@
                 .build();
     }
 
+    private NetworkRequest.Builder getBaseWifiNetworkRequestBuilder() {
+        return getBaseNetworkRequestBuilder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup));
+    }
+
     /**
      * Builds the WiFi bringup request
      *
@@ -224,10 +297,7 @@
      * but will NEVER bring up a Carrier WiFi network itself.
      */
     private NetworkRequest getWifiNetworkRequest() {
-        return getBaseNetworkRequestBuilder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
-                .build();
+        return getBaseWifiNetworkRequestBuilder().build();
     }
 
     /**
@@ -238,9 +308,7 @@
      * pace to effectively select a short-lived WiFi offload network.
      */
     private NetworkRequest getWifiEntryRssiThresholdNetworkRequest() {
-        return getBaseNetworkRequestBuilder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
+        return getBaseWifiNetworkRequestBuilder()
                 // Ensure wifi updates signal strengths when crossing this threshold.
                 .setSignalStrength(getWifiEntryRssiThreshold(mCarrierConfig))
                 .build();
@@ -254,9 +322,7 @@
      * pace to effectively select away from a failing WiFi network.
      */
     private NetworkRequest getWifiExitRssiThresholdNetworkRequest() {
-        return getBaseNetworkRequestBuilder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
+        return getBaseWifiNetworkRequestBuilder()
                 // Ensure wifi updates signal strengths when crossing this threshold.
                 .setSignalStrength(getWifiExitRssiThreshold(mCarrierConfig))
                 .build();
@@ -273,11 +339,25 @@
      * <p>Since this request MUST make it to the TelephonyNetworkFactory, subIds are not specified
      * in the NetworkCapabilities, but rather in the TelephonyNetworkSpecifier.
      */
-    private NetworkRequest getCellNetworkRequestForSubId(int subId) {
-        return getBaseNetworkRequestBuilder()
-                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(subId))
-                .build();
+    private NetworkRequest getCellNetworkRequestForSubId(
+            int subId, Set<CapabilityMatchCriteria> capsMatchCriteria) {
+        final NetworkRequest.Builder nrBuilder =
+                getBaseNetworkRequestBuilder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                        .setNetworkSpecifier(new TelephonyNetworkSpecifier(subId));
+
+        for (CapabilityMatchCriteria capMatchCriteria : capsMatchCriteria) {
+            final int cap = capMatchCriteria.capability;
+            final int matchCriteria = capMatchCriteria.matchCriteria;
+
+            if (matchCriteria == MATCH_REQUIRED) {
+                nrBuilder.addCapability(cap);
+            } else if (matchCriteria == MATCH_FORBIDDEN) {
+                nrBuilder.addForbiddenCapability(cap);
+            }
+        }
+
+        return nrBuilder.build();
     }
 
     /**
@@ -285,7 +365,6 @@
      */
     private NetworkRequest.Builder getBaseNetworkRequestBuilder() {
         return new NetworkRequest.Builder()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
@@ -356,7 +435,7 @@
             if (!allNetworkPriorities.isEmpty()) {
                 allNetworkPriorities += ", ";
             }
-            allNetworkPriorities += record.network + ": " + record.getPriorityClass();
+            allNetworkPriorities += record.network + ": " + record.priorityClass;
         }
         logInfo(
                 "Selected network changed to "
@@ -393,19 +472,22 @@
 
         private TreeSet<UnderlyingNetworkRecord> getSortedUnderlyingNetworks() {
             TreeSet<UnderlyingNetworkRecord> sorted =
-                    new TreeSet<>(
-                            UnderlyingNetworkRecord.getComparator(
+                    new TreeSet<>(UnderlyingNetworkRecord.getComparator());
+
+            for (UnderlyingNetworkRecord.Builder builder :
+                    mUnderlyingNetworkRecordBuilders.values()) {
+                if (builder.isValid()) {
+                    final UnderlyingNetworkRecord record =
+                            builder.build(
                                     mVcnContext,
                                     mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
                                     mSubscriptionGroup,
                                     mLastSnapshot,
                                     mCurrentRecord,
-                                    mCarrierConfig));
-
-            for (UnderlyingNetworkRecord.Builder builder :
-                    mUnderlyingNetworkRecordBuilders.values()) {
-                if (builder.isValid()) {
-                    sorted.add(builder.build());
+                                    mCarrierConfig);
+                    if (record.priorityClass != NetworkPriorityClassifier.PRIORITY_INVALID) {
+                        sorted.add(record);
+                    }
                 }
             }
 
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
index 319680e..aea9f4d 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -42,53 +42,58 @@
  * @hide
  */
 public class UnderlyingNetworkRecord {
-    private static final int PRIORITY_CLASS_INVALID = Integer.MAX_VALUE;
-
     @NonNull public final Network network;
     @NonNull public final NetworkCapabilities networkCapabilities;
     @NonNull public final LinkProperties linkProperties;
     public final boolean isBlocked;
-
-    private int mPriorityClass = PRIORITY_CLASS_INVALID;
+    public final boolean isSelected;
+    public final int priorityClass;
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public UnderlyingNetworkRecord(
             @NonNull Network network,
             @NonNull NetworkCapabilities networkCapabilities,
             @NonNull LinkProperties linkProperties,
-            boolean isBlocked) {
-        this.network = network;
-        this.networkCapabilities = networkCapabilities;
-        this.linkProperties = linkProperties;
-        this.isBlocked = isBlocked;
-    }
-
-    private int getOrCalculatePriorityClass(
+            boolean isBlocked,
             VcnContext vcnContext,
             List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
             PersistableBundleWrapper carrierConfig) {
-        // Never changes after the underlying network record is created.
-        if (mPriorityClass == PRIORITY_CLASS_INVALID) {
-            mPriorityClass =
-                    NetworkPriorityClassifier.calculatePriorityClass(
-                            vcnContext,
-                            this,
-                            underlyingNetworkTemplates,
-                            subscriptionGroup,
-                            snapshot,
-                            currentlySelected,
-                            carrierConfig);
-        }
+        this.network = network;
+        this.networkCapabilities = networkCapabilities;
+        this.linkProperties = linkProperties;
+        this.isBlocked = isBlocked;
 
-        return mPriorityClass;
+        this.isSelected = isSelected(this.network, currentlySelected);
+
+        priorityClass =
+                NetworkPriorityClassifier.calculatePriorityClass(
+                        vcnContext,
+                        this,
+                        underlyingNetworkTemplates,
+                        subscriptionGroup,
+                        snapshot,
+                        currentlySelected,
+                        carrierConfig);
     }
 
-    // Used in UnderlyingNetworkController
-    int getPriorityClass() {
-        return mPriorityClass;
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public UnderlyingNetworkRecord(
+            @NonNull Network network,
+            @NonNull NetworkCapabilities networkCapabilities,
+            @NonNull LinkProperties linkProperties,
+            boolean isBlocked,
+            boolean isSelected,
+            int priorityClass) {
+        this.network = network;
+        this.networkCapabilities = networkCapabilities;
+        this.linkProperties = linkProperties;
+        this.isBlocked = isBlocked;
+        this.isSelected = isSelected;
+
+        this.priorityClass = priorityClass;
     }
 
     @Override
@@ -108,40 +113,32 @@
         return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
     }
 
-    static Comparator<UnderlyingNetworkRecord> getComparator(
-            VcnContext vcnContext,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            ParcelUuid subscriptionGroup,
-            TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
-            PersistableBundleWrapper carrierConfig) {
+    /** Returns if two records are equal including their priority classes. */
+    public static boolean isEqualIncludingPriorities(
+            UnderlyingNetworkRecord left, UnderlyingNetworkRecord right) {
+        if (left != null && right != null) {
+            return left.equals(right)
+                    && left.isSelected == right.isSelected
+                    && left.priorityClass == right.priorityClass;
+        }
+
+        return left == right;
+    }
+
+    static Comparator<UnderlyingNetworkRecord> getComparator() {
         return (left, right) -> {
-            final int leftIndex =
-                    left.getOrCalculatePriorityClass(
-                            vcnContext,
-                            underlyingNetworkTemplates,
-                            subscriptionGroup,
-                            snapshot,
-                            currentlySelected,
-                            carrierConfig);
-            final int rightIndex =
-                    right.getOrCalculatePriorityClass(
-                            vcnContext,
-                            underlyingNetworkTemplates,
-                            subscriptionGroup,
-                            snapshot,
-                            currentlySelected,
-                            carrierConfig);
+            final int leftIndex = left.priorityClass;
+            final int rightIndex = right.priorityClass;
 
             // In the case of networks in the same priority class, prioritize based on other
             // criteria (eg. actively selected network, link metrics, etc)
             if (leftIndex == rightIndex) {
                 // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
                 // fall into the same priority class.
-                if (isSelected(left, currentlySelected)) {
+                if (left.isSelected) {
                     return -1;
                 }
-                if (isSelected(left, currentlySelected)) {
+                if (right.isSelected) {
                     return 1;
                 }
             }
@@ -150,11 +147,11 @@
     }
 
     private static boolean isSelected(
-            UnderlyingNetworkRecord recordToCheck, UnderlyingNetworkRecord currentlySelected) {
+            Network networkToCheck, UnderlyingNetworkRecord currentlySelected) {
         if (currentlySelected == null) {
             return false;
         }
-        if (currentlySelected.network == recordToCheck.network) {
+        if (currentlySelected.network.equals(networkToCheck)) {
             return true;
         }
         return false;
@@ -172,16 +169,8 @@
         pw.println("UnderlyingNetworkRecord:");
         pw.increaseIndent();
 
-        final int priorityIndex =
-                getOrCalculatePriorityClass(
-                        vcnContext,
-                        underlyingNetworkTemplates,
-                        subscriptionGroup,
-                        snapshot,
-                        currentlySelected,
-                        carrierConfig);
-
-        pw.println("Priority index: " + priorityIndex);
+        pw.println("priorityClass: " + priorityClass);
+        pw.println("isSelected: " + isSelected);
         pw.println("mNetwork: " + network);
         pw.println("mNetworkCapabilities: " + networkCapabilities);
         pw.println("mLinkProperties: " + linkProperties);
@@ -198,8 +187,6 @@
         boolean mIsBlocked;
         boolean mWasIsBlockedSet;
 
-        @Nullable private UnderlyingNetworkRecord mCached;
-
         Builder(@NonNull Network network) {
             mNetwork = network;
         }
@@ -211,7 +198,6 @@
 
         void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
             mNetworkCapabilities = networkCapabilities;
-            mCached = null;
         }
 
         @Nullable
@@ -221,32 +207,40 @@
 
         void setLinkProperties(@NonNull LinkProperties linkProperties) {
             mLinkProperties = linkProperties;
-            mCached = null;
         }
 
         void setIsBlocked(boolean isBlocked) {
             mIsBlocked = isBlocked;
             mWasIsBlockedSet = true;
-            mCached = null;
         }
 
         boolean isValid() {
             return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet;
         }
 
-        UnderlyingNetworkRecord build() {
+        UnderlyingNetworkRecord build(
+                VcnContext vcnContext,
+                List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+                ParcelUuid subscriptionGroup,
+                TelephonySubscriptionSnapshot snapshot,
+                UnderlyingNetworkRecord currentlySelected,
+                PersistableBundleWrapper carrierConfig) {
             if (!isValid()) {
                 throw new IllegalArgumentException(
                         "Called build before UnderlyingNetworkRecord was valid");
             }
 
-            if (mCached == null) {
-                mCached =
-                        new UnderlyingNetworkRecord(
-                                mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
-            }
-
-            return mCached;
+            return new UnderlyingNetworkRecord(
+                    mNetwork,
+                    mNetworkCapabilities,
+                    mLinkProperties,
+                    mIsBlocked,
+                    vcnContext,
+                    underlyingNetworkTemplates,
+                    subscriptionGroup,
+                    snapshot,
+                    currentlySelected,
+                    carrierConfig);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
index d22ec0a..d6761a2 100644
--- a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
+++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
@@ -573,5 +573,10 @@
 
             return isEqual(mBundle, other.mBundle);
         }
+
+        @Override
+        public String toString() {
+            return mBundle.toString();
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index ff09163..330c098 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1085,10 +1085,14 @@
         // Use launch-adjacent-flag-root if launching with launch-adjacent flag.
         if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
                 && mLaunchAdjacentFlagRootTask != null) {
-            // If the adjacent launch is coming from the same root, launch to adjacent root instead.
-            if (sourceTask != null && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null
+            if (sourceTask != null && sourceTask == candidateTask) {
+                // Do nothing when task that is getting opened is same as the source.
+            } else if (sourceTask != null
+                    && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null
                     && (sourceTask == mLaunchAdjacentFlagRootTask
                     || sourceTask.isDescendantOf(mLaunchAdjacentFlagRootTask))) {
+                // If the adjacent launch is coming from the same root, launch to
+                // adjacent root instead.
                 return mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment().asTask();
             } else {
                 return mLaunchAdjacentFlagRootTask;
diff --git a/telephony/java/android/telephony/AccessNetworkConstants.java b/telephony/java/android/telephony/AccessNetworkConstants.java
index 0fdf40d..27ba676 100644
--- a/telephony/java/android/telephony/AccessNetworkConstants.java
+++ b/telephony/java/android/telephony/AccessNetworkConstants.java
@@ -605,9 +605,9 @@
         EUTRAN_ARFCN_FREQUENCY_BAND_41(
                 EutranBand.BAND_41, 2496000, 39650, 41589, 2496000, 39650, 41589),
         EUTRAN_ARFCN_FREQUENCY_BAND_42(
-                EutranBand.BAND_42, 3400000, 41950, 43589, 3400000, 41950, 43589),
+                EutranBand.BAND_42, 3400000, 41590, 43589, 3400000, 41590, 43589),
         EUTRAN_ARFCN_FREQUENCY_BAND_43(
-                EutranBand.BAND_43, 3600000, 43950, 45589, 3600000, 43950, 45589),
+                EutranBand.BAND_43, 3600000, 43590, 45589, 3600000, 43590, 45589),
         EUTRAN_ARFCN_FREQUENCY_BAND_44(
                 EutranBand.BAND_44, 703000, 45590, 46589, 703000, 45590, 46589),
         EUTRAN_ARFCN_FREQUENCY_BAND_45(
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 12d1bc3..d8c1b57e3 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -4308,6 +4308,18 @@
             "data_switch_validation_timeout_long";
 
     /**
+     * The minimum timeout of UDP port 4500 NAT / firewall entries on the Internet PDN of this
+     * carrier network. This will be used by Android platform VPNs to tune IPsec NAT keepalive
+     * interval. If this value is too low to provide uninterrupted inbound connectivity, then
+     * Android system VPNs may indicate to applications that the VPN cannot support long-lived
+     * TCP connections.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final String KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT =
+            "min_udp_port_4500_nat_timeout_sec_int";
+
+    /**
      * Specifies whether the system should prefix the EAP method to the anonymous identity.
      * The following prefix will be added if this key is set to TRUE:
      *   EAP-AKA: "0"
@@ -8008,6 +8020,14 @@
         public static final String KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL =
                 KEY_PREFIX + "supports_eap_aka_fast_reauth_bool";
 
+        /**
+         * Type of IP preference used to prioritize ePDG servers. Possible values are
+         * {@link #EPDG_ADDRESS_IPV4_PREFERRED}, {@link #EPDG_ADDRESS_IPV6_PREFERRED},
+         * {@link #EPDG_ADDRESS_IPV4_ONLY}
+         */
+        public static final String KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT =
+                KEY_PREFIX + "epdg_address_ip_type_preference_int";
+
         /** @hide */
         @IntDef({AUTHENTICATION_METHOD_EAP_ONLY, AUTHENTICATION_METHOD_CERT})
         public @interface AuthenticationMethodType {}
@@ -8072,6 +8092,39 @@
          */
         public static final int ID_TYPE_KEY_ID = 11;
 
+        /** @hide */
+        @IntDef({
+                EPDG_ADDRESS_IPV4_PREFERRED,
+                EPDG_ADDRESS_IPV6_PREFERRED,
+                EPDG_ADDRESS_IPV4_ONLY,
+                EPDG_ADDRESS_IPV6_ONLY,
+                EPDG_ADDRESS_SYSTEM_PREFERRED
+        })
+        public @interface EpdgAddressIpPreference {}
+
+        /** Prioritize IPv4 ePDG addresses. */
+        public static final int EPDG_ADDRESS_IPV4_PREFERRED = 0;
+
+        /** Prioritize IPv6 ePDG addresses */
+        public static final int EPDG_ADDRESS_IPV6_PREFERRED = 1;
+
+        /** Use IPv4 ePDG addresses only. */
+        public static final int EPDG_ADDRESS_IPV4_ONLY = 2;
+
+        /** Use IPv6 ePDG addresses only.
+         * @hide
+         */
+        public static final int EPDG_ADDRESS_IPV6_ONLY = 3;
+
+        /** Follow the priority from DNS resolution results, which are sorted by using RFC6724
+         * algorithm.
+         *
+         * @see <a href="https://tools.ietf.org/html/rfc6724#section-6">RFC 6724, Default Address
+         *     Selection for Internet Protocol Version 6 (IPv6)</a>
+         * @hide
+         */
+        public static final int EPDG_ADDRESS_SYSTEM_PREFERRED = 4;
+
         private Iwlan() {}
 
         private static PersistableBundle getDefaults() {
@@ -8155,7 +8208,7 @@
             defaults.putInt(KEY_EPDG_PCO_ID_IPV6_INT, 0);
             defaults.putInt(KEY_EPDG_PCO_ID_IPV4_INT, 0);
             defaults.putBoolean(KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL, false);
-
+            defaults.putInt(KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT, EPDG_ADDRESS_IPV4_PREFERRED);
             return defaults;
         }
     }
@@ -9130,6 +9183,7 @@
         sDefaults.putStringArray(KEY_MMI_TWO_DIGIT_NUMBER_PATTERN_STRING_ARRAY, new String[0]);
         sDefaults.putInt(KEY_PARAMETERS_USED_FOR_LTE_SIGNAL_BAR_INT,
                 CellSignalStrengthLte.USE_RSRP);
+        sDefaults.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, 300);
         // Default wifi configurations.
         sDefaults.putAll(Wifi.getDefaults());
         sDefaults.putBoolean(ENABLE_EAP_METHOD_PREFIX_BOOL, false);
diff --git a/telephony/java/android/telephony/data/QosBearerFilter.java b/telephony/java/android/telephony/data/QosBearerFilter.java
index 0ab7b61..a0d9c1bd 100644
--- a/telephony/java/android/telephony/data/QosBearerFilter.java
+++ b/telephony/java/android/telephony/data/QosBearerFilter.java
@@ -130,6 +130,10 @@
         return precedence;
     }
 
+    public int getProtocol() {
+        return protocol;
+    }
+
     public static class PortRange implements Parcelable {
         int start;
         int end;
diff --git a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
index 1f6bb21..1569613 100644
--- a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
@@ -32,7 +32,8 @@
     private static final Set<String> ALLOWED_PLMN_IDS = new HashSet<>();
     private static final Set<Integer> ALLOWED_CARRIER_IDS = new HashSet<>();
 
-    private static VcnCellUnderlyingNetworkTemplate.Builder getTestNetworkTemplateBuilder() {
+    // Public for use in UnderlyingNetworkControllerTest
+    public static VcnCellUnderlyingNetworkTemplate.Builder getTestNetworkTemplateBuilder() {
         return new VcnCellUnderlyingNetworkTemplate.Builder()
                 .setMetered(MATCH_FORBIDDEN)
                 .setMinUpstreamBandwidthKbps(
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 4040888..1883c85 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -100,14 +100,20 @@
                 EXPOSED_CAPS);
     }
 
-    // Public for use in VcnGatewayConnectionTest
-    public static VcnGatewayConnectionConfig buildTestConfig() {
+    // Public for use in UnderlyingNetworkControllerTest
+    public static VcnGatewayConnectionConfig buildTestConfig(
+            List<VcnUnderlyingNetworkTemplate> nwTemplates) {
         final VcnGatewayConnectionConfig.Builder builder =
-                newBuilder().setVcnUnderlyingNetworkPriorities(UNDERLYING_NETWORK_TEMPLATES);
+                newBuilder().setVcnUnderlyingNetworkPriorities(nwTemplates);
 
         return buildTestConfigWithExposedCaps(builder, EXPOSED_CAPS);
     }
 
+    // Public for use in VcnGatewayConnectionTest
+    public static VcnGatewayConnectionConfig buildTestConfig() {
+        return buildTestConfig(UNDERLYING_NETWORK_TEMPLATES);
+    }
+
     private static VcnGatewayConnectionConfig.Builder newBuilder() {
         // Append a unique identifier to the name prefix to guarantee that all created
         // VcnGatewayConnectionConfigs have a unique name (required by VcnConfig).
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 1c21a06..aad7a5e 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -59,6 +59,7 @@
 import android.net.NetworkCapabilities;
 import android.net.ipsec.ike.ChildSaProposal;
 import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
 import android.net.ipsec.ike.exceptions.IkeException;
 import android.net.ipsec.ike.exceptions.IkeInternalException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
@@ -70,6 +71,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
 import com.android.server.vcn.util.MtuUtils;
 
@@ -90,6 +92,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
+    private static final int PARALLEL_SA_COUNT = 4;
+
     private VcnIkeSession mIkeSession;
     private VcnNetworkAgent mNetworkAgent;
     private Network mVcnNetwork;
@@ -227,16 +231,29 @@
     private void verifyVcnTransformsApplied(
             VcnGatewayConnection vcnGatewayConnection, boolean expectForwardTransform)
             throws Exception {
+        verifyVcnTransformsApplied(
+                vcnGatewayConnection,
+                expectForwardTransform,
+                Collections.singletonList(getChildSessionCallback()));
+    }
+
+    private void verifyVcnTransformsApplied(
+            VcnGatewayConnection vcnGatewayConnection,
+            boolean expectForwardTransform,
+            List<VcnChildSessionCallback> callbacks)
+            throws Exception {
         for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT}) {
-            getChildSessionCallback().onIpSecTransformCreated(makeDummyIpSecTransform(), direction);
+            for (VcnChildSessionCallback cb : callbacks) {
+                cb.onIpSecTransformCreated(makeDummyIpSecTransform(), direction);
+            }
             mTestLooper.dispatchAll();
 
-            verify(mIpSecSvc)
+            verify(mIpSecSvc, times(callbacks.size()))
                     .applyTunnelModeTransform(
                             eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any());
         }
 
-        verify(mIpSecSvc, expectForwardTransform ? times(1) : never())
+        verify(mIpSecSvc, expectForwardTransform ? times(callbacks.size()) : never())
                 .applyTunnelModeTransform(
                         eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(DIRECTION_FWD), anyInt(), any());
 
@@ -416,6 +433,89 @@
         verifySafeModeStateAndCallbackFired(1 /* invocationCount */, false /* isInSafeMode */);
     }
 
+    private List<VcnChildSessionCallback> openChildAndVerifyParallelSasRequested()
+            throws Exception {
+        doReturn(PARALLEL_SA_COUNT)
+                .when(mDeps)
+                .getParallelTunnelCount(eq(TEST_SUBSCRIPTION_SNAPSHOT), eq(TEST_SUB_GRP));
+
+        // Verify scheduled but not canceled when entering ConnectedState
+        verifySafeModeTimeoutAlarmAndGetCallback(false /* expectCanceled */);
+        triggerChildOpened();
+        mTestLooper.dispatchAll();
+
+        // Verify new child sessions requested
+        final ArgumentCaptor<VcnChildSessionCallback> captor =
+                ArgumentCaptor.forClass(VcnChildSessionCallback.class);
+        verify(mIkeSession, times(PARALLEL_SA_COUNT - 1))
+                .openChildSession(any(TunnelModeChildSessionParams.class), captor.capture());
+
+        return captor.getAllValues();
+    }
+
+    private List<VcnChildSessionCallback> verifyChildOpenedRequestsAndAppliesParallelSas()
+            throws Exception {
+        List<VcnChildSessionCallback> callbacks = openChildAndVerifyParallelSasRequested();
+
+        verifyVcnTransformsApplied(mGatewayConnection, false, callbacks);
+
+        // Mock IKE calling of onOpened()
+        for (VcnChildSessionCallback cb : callbacks) {
+            cb.onOpened(mock(VcnChildSessionConfiguration.class));
+        }
+        mTestLooper.dispatchAll();
+
+        assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
+        return callbacks;
+    }
+
+    @Test
+    public void testChildOpenedWithParallelSas() throws Exception {
+        verifyChildOpenedRequestsAndAppliesParallelSas();
+    }
+
+    @Test
+    public void testOpportunisticSa_ignoresPreOpenFailures() throws Exception {
+        List<VcnChildSessionCallback> callbacks = openChildAndVerifyParallelSasRequested();
+
+        for (VcnChildSessionCallback cb : callbacks) {
+            cb.onClosed();
+            cb.onClosedExceptionally(mock(IkeException.class));
+        }
+        mTestLooper.dispatchAll();
+
+        assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
+        assertEquals(mIkeConnectionInfo, mGatewayConnection.getIkeConnectionInfo());
+    }
+
+    private void verifyPostOpenFailuresCloseSession(boolean shouldCloseWithException)
+            throws Exception {
+        List<VcnChildSessionCallback> callbacks = verifyChildOpenedRequestsAndAppliesParallelSas();
+
+        for (VcnChildSessionCallback cb : callbacks) {
+            if (shouldCloseWithException) {
+                cb.onClosed();
+            } else {
+                cb.onClosedExceptionally(mock(IkeException.class));
+            }
+        }
+        mTestLooper.dispatchAll();
+
+        assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState());
+        verify(mIkeSession).close();
+    }
+
+    @Test
+    public void testOpportunisticSa_handlesPostOpenFailures_onClosed() throws Exception {
+        verifyPostOpenFailuresCloseSession(false /* shouldCloseWithException */);
+    }
+
+    @Test
+    public void testOpportunisticSa_handlesPostOpenFailures_onClosedExceptionally()
+            throws Exception {
+        verifyPostOpenFailuresCloseSession(true /* shouldCloseWithException */);
+    }
+
     @Test
     public void testInternalAndDnsAddressesChanged() throws Exception {
         final List<LinkAddress> startingInternalAddrs =
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index a4ee2de..692c8a8 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -143,9 +143,9 @@
         capBuilder.setLinkDownstreamBandwidthKbps(TEST_DOWNSTREAM_BANDWIDTH);
         capBuilder.setAdministratorUids(new int[] {TEST_UID});
         final Network underlyingNetwork = mock(Network.class, CALLS_REAL_METHODS);
-        UnderlyingNetworkRecord record = new UnderlyingNetworkRecord(
-                underlyingNetwork,
-                capBuilder.build(), new LinkProperties(), false);
+        UnderlyingNetworkRecord record =
+                getTestNetworkRecord(
+                        underlyingNetwork, capBuilder.build(), new LinkProperties(), false);
         final NetworkCapabilities vcnCaps =
                 VcnGatewayConnection.buildNetworkCapabilities(
                         VcnGatewayConnectionConfigTest.buildTestConfig(),
@@ -211,7 +211,7 @@
         doReturn(TEST_DNS_ADDRESSES).when(childSessionConfig).getInternalDnsServers();
 
         UnderlyingNetworkRecord record =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mock(Network.class, CALLS_REAL_METHODS),
                         new NetworkCapabilities.Builder().build(),
                         underlyingLp,
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 7bafd24..bb123ff 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -109,9 +109,23 @@
     protected static final long ELAPSED_REAL_TIME = 123456789L;
     protected static final String TEST_IPSEC_TUNNEL_IFACE = "IPSEC_IFACE";
 
+    protected static UnderlyingNetworkRecord getTestNetworkRecord(
+            Network network,
+            NetworkCapabilities networkCapabilities,
+            LinkProperties linkProperties,
+            boolean isBlocked) {
+        return new UnderlyingNetworkRecord(
+                network,
+                networkCapabilities,
+                linkProperties,
+                isBlocked,
+                false /* isSelected */,
+                0 /* priorityClass */);
+    }
+
     protected static final String TEST_TCP_BUFFER_SIZES_1 = "1,2,3,4";
     protected static final UnderlyingNetworkRecord TEST_UNDERLYING_NETWORK_RECORD_1 =
-            new UnderlyingNetworkRecord(
+            getTestNetworkRecord(
                     mock(Network.class, CALLS_REAL_METHODS),
                     new NetworkCapabilities(),
                     new LinkProperties(),
@@ -124,7 +138,7 @@
 
     protected static final String TEST_TCP_BUFFER_SIZES_2 = "2,3,4,5";
     protected static final UnderlyingNetworkRecord TEST_UNDERLYING_NETWORK_RECORD_2 =
-            new UnderlyingNetworkRecord(
+            getTestNetworkRecord(
                     mock(Network.class, CALLS_REAL_METHODS),
                     new NetworkCapabilities(),
                     new LinkProperties(),
@@ -209,6 +223,9 @@
         doReturn(mWakeLock)
                 .when(mDeps)
                 .newWakeLock(eq(mContext), eq(PowerManager.PARTIAL_WAKE_LOCK), any());
+        doReturn(1)
+                .when(mDeps)
+                .getParallelTunnelCount(eq(TEST_SUBSCRIPTION_SNAPSHOT), eq(TEST_SUB_GRP));
 
         setUpWakeupMessage(mTeardownTimeoutAlarm, VcnGatewayConnection.TEARDOWN_TIMEOUT_ALARM);
         setUpWakeupMessage(mDisconnectRequestAlarm, VcnGatewayConnection.DISCONNECT_REQUEST_ALARM);
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
index b0d6895..629e988 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vcn.routeselection;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_FORBIDDEN;
 import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED;
 import static android.net.vcn.VcnUnderlyingNetworkTemplateTestBase.TEST_MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS;
@@ -24,8 +25,8 @@
 import static android.net.vcn.VcnUnderlyingNetworkTemplateTestBase.TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS;
 
 import static com.android.server.vcn.VcnTestUtils.setupSystemService;
-import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_ANY;
-import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.calculatePriorityClass;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_FALLBACK;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesCellPriorityRule;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesPriorityRule;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesWifiPriorityRule;
@@ -48,6 +49,7 @@
 import android.net.vcn.VcnCellUnderlyingNetworkTemplate;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnManager;
+import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.net.vcn.VcnWifiUnderlyingNetworkTemplate;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
@@ -64,6 +66,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 import java.util.UUID;
 
@@ -102,6 +106,7 @@
     private static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
             new NetworkCapabilities.Builder()
                     .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
                     .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                     .setSubscriptionIds(Set.of(SUB_ID))
                     .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
@@ -135,25 +140,35 @@
                                 false /* isInTestMode */));
         doNothing().when(mVcnContext).ensureRunningOnLooperThread();
 
-        mWifiNetworkRecord =
-                new UnderlyingNetworkRecord(
-                        mNetwork,
-                        WIFI_NETWORK_CAPABILITIES,
-                        LINK_PROPERTIES,
-                        false /* isBlocked */);
-
-        mCellNetworkRecord =
-                new UnderlyingNetworkRecord(
-                        mNetwork,
-                        CELL_NETWORK_CAPABILITIES,
-                        LINK_PROPERTIES,
-                        false /* isBlocked */);
-
         setupSystemService(
                 mockContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
         when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
         when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
         when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
+
+        mWifiNetworkRecord =
+                getTestNetworkRecord(
+                        WIFI_NETWORK_CAPABILITIES,
+                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES);
+        mCellNetworkRecord =
+                getTestNetworkRecord(
+                        CELL_NETWORK_CAPABILITIES,
+                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES);
+    }
+
+    private UnderlyingNetworkRecord getTestNetworkRecord(
+            NetworkCapabilities nc, List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates) {
+        return new UnderlyingNetworkRecord(
+                mNetwork,
+                nc,
+                LINK_PROPERTIES,
+                false /* isBlocked */,
+                mVcnContext,
+                underlyingNetworkTemplates,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                null /* currentlySelected */,
+                null /* carrierConfig */);
     }
 
     @Test
@@ -490,37 +505,72 @@
                         mSubscriptionSnapshot));
     }
 
-    private void verifyCalculatePriorityClass(
-            UnderlyingNetworkRecord networkRecord, int expectedIndex) {
-        final int priorityIndex =
-                calculatePriorityClass(
+    private void verifyMatchCellWithRequiredCapabilities(
+            VcnCellUnderlyingNetworkTemplate template, boolean expectMatch) {
+        assertEquals(
+                expectMatch,
+                checkMatchesCellPriorityRule(
                         mVcnContext,
-                        networkRecord,
-                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                        template,
+                        mCellNetworkRecord,
                         SUB_GROUP,
-                        mSubscriptionSnapshot,
-                        null /* currentlySelected */,
-                        null /* carrierConfig */);
+                        mSubscriptionSnapshot));
+    }
 
-        assertEquals(expectedIndex, priorityIndex);
+    @Test
+    public void testMatchCell() {
+        final VcnCellUnderlyingNetworkTemplate template =
+                getCellNetworkPriorityBuilder().setInternet(MATCH_REQUIRED).build();
+        verifyMatchCellWithRequiredCapabilities(template, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchCellFail_RequiredCapabilitiesMissing() {
+        final VcnCellUnderlyingNetworkTemplate template =
+                getCellNetworkPriorityBuilder().setCbs(MATCH_REQUIRED).build();
+        verifyMatchCellWithRequiredCapabilities(template, false /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchCellFail_ForbiddenCapabilitiesFound() {
+        final VcnCellUnderlyingNetworkTemplate template =
+                getCellNetworkPriorityBuilder().setDun(MATCH_FORBIDDEN).build();
+        verifyMatchCellWithRequiredCapabilities(template, false /* expectMatch */);
     }
 
     @Test
     public void testCalculatePriorityClass() throws Exception {
-        verifyCalculatePriorityClass(mCellNetworkRecord, 2);
+        assertEquals(2, mCellNetworkRecord.priorityClass);
+    }
+
+    private void checkCalculatePriorityClassFailToMatchAny(
+            boolean hasInternet, int expectedPriorityClass) throws Exception {
+        final List<VcnUnderlyingNetworkTemplate> templatesRequireDun =
+                Collections.singletonList(
+                        new VcnCellUnderlyingNetworkTemplate.Builder()
+                                .setDun(MATCH_REQUIRED)
+                                .build());
+
+        final NetworkCapabilities.Builder ncBuilder =
+                new NetworkCapabilities.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        if (hasInternet) {
+            ncBuilder.addCapability(NET_CAPABILITY_INTERNET);
+        }
+
+        final UnderlyingNetworkRecord nonDunNetworkRecord =
+                getTestNetworkRecord(ncBuilder.build(), templatesRequireDun);
+
+        assertEquals(expectedPriorityClass, nonDunNetworkRecord.priorityClass);
     }
 
     @Test
-    public void testCalculatePriorityClassFailToMatchAny() throws Exception {
-        final NetworkCapabilities nc =
-                new NetworkCapabilities.Builder()
-                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                        .setSignalStrength(WIFI_RSSI_LOW)
-                        .setSsid(SSID)
-                        .build();
-        final UnderlyingNetworkRecord wifiNetworkRecord =
-                new UnderlyingNetworkRecord(mNetwork, nc, LINK_PROPERTIES, false /* isBlocked */);
+    public void testCalculatePriorityClassFailToMatchAny_InternetNetwork() throws Exception {
+        checkCalculatePriorityClassFailToMatchAny(true /* hasInternet */, PRIORITY_FALLBACK);
+    }
 
-        verifyCalculatePriorityClass(wifiNetworkRecord, PRIORITY_ANY);
+    @Test
+    public void testCalculatePriorityClassFailToMatchAny_NonInternetNetwork() throws Exception {
+        checkCalculatePriorityClassFailToMatchAny(false /* hasInternet */, PRIORITY_INVALID);
     }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index fad9669..2941fde 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -16,18 +16,29 @@
 
 package com.android.server.vcn.routeselection;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
+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.vcn.VcnCellUnderlyingNetworkTemplate.MATCH_ANY;
+import static android.net.vcn.VcnCellUnderlyingNetworkTemplate.MATCH_FORBIDDEN;
+import static android.net.vcn.VcnCellUnderlyingNetworkTemplate.MATCH_REQUIRED;
+
 import static com.android.server.vcn.VcnTestUtils.setupSystemService;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -42,7 +53,10 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnCellUnderlyingNetworkTemplate;
+import android.net.vcn.VcnCellUnderlyingNetworkTemplateTest;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
+import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.os.ParcelUuid;
 import android.os.test.TestLooper;
 import android.telephony.CarrierConfigManager;
@@ -64,7 +78,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 import java.util.UUID;
 
@@ -95,11 +112,39 @@
                     .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                     .build();
 
+    private static final NetworkCapabilities DUN_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                    .build();
+
+    private static final NetworkCapabilities CBS_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                    .build();
+
     private static final LinkProperties INITIAL_LINK_PROPERTIES =
             getLinkPropertiesWithName("initial_iface");
     private static final LinkProperties UPDATED_LINK_PROPERTIES =
             getLinkPropertiesWithName("updated_iface");
 
+    private static final VcnCellUnderlyingNetworkTemplate CELL_TEMPLATE_DUN =
+            new VcnCellUnderlyingNetworkTemplate.Builder()
+                    .setInternet(MATCH_ANY)
+                    .setDun(MATCH_REQUIRED)
+                    .build();
+
+    private static final VcnCellUnderlyingNetworkTemplate CELL_TEMPLATE_CBS =
+            new VcnCellUnderlyingNetworkTemplate.Builder()
+                    .setInternet(MATCH_ANY)
+                    .setCbs(MATCH_REQUIRED)
+                    .build();
+
     @Mock private Context mContext;
     @Mock private VcnNetworkProvider mVcnNetworkProvider;
     @Mock private ConnectivityManager mConnectivityManager;
@@ -201,6 +246,107 @@
                         any());
     }
 
+    private void verifyRequestBackgroundNetwork(
+            ConnectivityManager cm,
+            int expectedSubId,
+            Set<Integer> expectedRequiredCaps,
+            Set<Integer> expectedForbiddenCaps) {
+        verify(cm)
+                .requestBackgroundNetwork(
+                        eq(
+                                getCellRequestForSubId(
+                                        expectedSubId,
+                                        expectedRequiredCaps,
+                                        expectedForbiddenCaps)),
+                        any(NetworkBringupCallback.class),
+                        any());
+    }
+
+    @Test
+    public void testNetworkCallbacksRegisteredOnStartupForNonInternetCapabilities() {
+        final ConnectivityManager cm = mock(ConnectivityManager.class);
+        setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
+
+        // Build network templates
+        final List<VcnUnderlyingNetworkTemplate> networkTemplates = new ArrayList();
+
+        networkTemplates.add(
+                VcnCellUnderlyingNetworkTemplateTest.getTestNetworkTemplateBuilder()
+                        .setDun(MATCH_REQUIRED)
+                        .setInternet(MATCH_ANY)
+                        .build());
+
+        networkTemplates.add(
+                VcnCellUnderlyingNetworkTemplateTest.getTestNetworkTemplateBuilder()
+                        .setMms(MATCH_REQUIRED)
+                        .setCbs(MATCH_FORBIDDEN)
+                        .setInternet(MATCH_ANY)
+                        .build());
+
+        // Start UnderlyingNetworkController
+        new UnderlyingNetworkController(
+                mVcnContext,
+                VcnGatewayConnectionConfigTest.buildTestConfig(networkTemplates),
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mNetworkControllerCb);
+
+        // Verifications
+        for (final int subId : INITIAL_SUB_IDS) {
+            verifyRequestBackgroundNetwork(
+                    cm,
+                    subId,
+                    Collections.singleton(NET_CAPABILITY_INTERNET),
+                    Collections.emptySet());
+            verifyRequestBackgroundNetwork(
+                    cm, subId, Collections.singleton(NET_CAPABILITY_DUN), Collections.emptySet());
+            verifyRequestBackgroundNetwork(
+                    cm,
+                    subId,
+                    Collections.singleton(NET_CAPABILITY_MMS),
+                    Collections.singleton(NET_CAPABILITY_CBS));
+        }
+    }
+
+    @Test
+    public void testNetworkCallbacksRegisteredOnStartupWithDedupedtCapabilities() {
+        final ConnectivityManager cm = mock(ConnectivityManager.class);
+        setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
+
+        // Build network templates
+        final List<VcnUnderlyingNetworkTemplate> networkTemplates = new ArrayList();
+        final VcnCellUnderlyingNetworkTemplate.Builder builder =
+                new VcnCellUnderlyingNetworkTemplate.Builder()
+                        .setMms(MATCH_REQUIRED)
+                        .setCbs(MATCH_FORBIDDEN)
+                        .setInternet(MATCH_ANY);
+
+        networkTemplates.add(builder.setMetered(MATCH_REQUIRED).build());
+        networkTemplates.add(builder.setMetered(MATCH_FORBIDDEN).build());
+
+        // Start UnderlyingNetworkController
+        new UnderlyingNetworkController(
+                mVcnContext,
+                VcnGatewayConnectionConfigTest.buildTestConfig(networkTemplates),
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mNetworkControllerCb);
+
+        // Verifications
+        for (final int subId : INITIAL_SUB_IDS) {
+            verifyRequestBackgroundNetwork(
+                    cm,
+                    subId,
+                    Collections.singleton(NET_CAPABILITY_INTERNET),
+                    Collections.emptySet());
+            verifyRequestBackgroundNetwork(
+                    cm,
+                    subId,
+                    Collections.singleton(NET_CAPABILITY_MMS),
+                    Collections.singleton(NET_CAPABILITY_CBS));
+        }
+    }
+
     private void verifyNetworkRequestsRegistered(Set<Integer> expectedSubIds) {
         verify(mConnectivityManager)
                 .requestBackgroundNetwork(
@@ -210,8 +356,13 @@
         for (final int subId : expectedSubIds) {
             verify(mConnectivityManager)
                     .requestBackgroundNetwork(
-                            eq(getCellRequestForSubId(subId)),
-                            any(NetworkBringupCallback.class), any());
+                            eq(
+                                    getCellRequestForSubId(
+                                            subId,
+                                            Collections.singleton(NET_CAPABILITY_INTERNET),
+                                            Collections.emptySet())),
+                            any(NetworkBringupCallback.class),
+                            any());
         }
 
         verify(mConnectivityManager)
@@ -253,6 +404,7 @@
     private NetworkRequest getWifiRequest(Set<Integer> netCapsSubIds) {
         return getExpectedRequestBase()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                 .setSubscriptionIds(netCapsSubIds)
                 .build();
     }
@@ -261,6 +413,7 @@
         // TODO (b/187991063): Add tests for carrier-config based thresholds
         return getExpectedRequestBase()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                 .setSubscriptionIds(netCapsSubIds)
                 .setSignalStrength(WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT)
                 .build();
@@ -270,16 +423,27 @@
         // TODO (b/187991063): Add tests for carrier-config based thresholds
         return getExpectedRequestBase()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                 .setSubscriptionIds(netCapsSubIds)
                 .setSignalStrength(WIFI_EXIT_RSSI_THRESHOLD_DEFAULT)
                 .build();
     }
 
-    private NetworkRequest getCellRequestForSubId(int subId) {
-        return getExpectedRequestBase()
-                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier(subId))
-                .build();
+    private NetworkRequest getCellRequestForSubId(
+            int subId, Set<Integer> requiredCaps, Set<Integer> forbiddenCaps) {
+        final NetworkRequest.Builder nqBuilder =
+                getExpectedRequestBase()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                        .setNetworkSpecifier(new TelephonyNetworkSpecifier(subId));
+
+        for (int cap : requiredCaps) {
+            nqBuilder.addCapability(cap);
+        }
+        for (int cap : forbiddenCaps) {
+            nqBuilder.addForbiddenCapability(cap);
+        }
+
+        return nqBuilder.build();
     }
 
     private NetworkRequest getRouteSelectionRequest(Set<Integer> netCapsSubIds) {
@@ -301,7 +465,6 @@
     private NetworkRequest.Builder getExpectedRequestBase() {
         final NetworkRequest.Builder builder =
                 new NetworkRequest.Builder()
-                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                         .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                         .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                         .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
@@ -321,16 +484,30 @@
                 .unregisterNetworkCallback(any(UnderlyingNetworkListener.class));
     }
 
+    private static UnderlyingNetworkRecord getTestNetworkRecord(
+            Network network,
+            NetworkCapabilities networkCapabilities,
+            LinkProperties linkProperties,
+            boolean isBlocked) {
+        return new UnderlyingNetworkRecord(
+                network,
+                networkCapabilities,
+                linkProperties,
+                isBlocked,
+                false /* isSelected */,
+                0 /* priorityClass */);
+    }
+
     @Test
     public void testUnderlyingNetworkRecordEquals() {
         UnderlyingNetworkRecord recordA =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         INITIAL_NETWORK_CAPABILITIES,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         UnderlyingNetworkRecord recordB =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         INITIAL_NETWORK_CAPABILITIES,
                         INITIAL_LINK_PROPERTIES,
@@ -338,12 +515,24 @@
         UnderlyingNetworkRecord recordC =
                 new UnderlyingNetworkRecord(
                         mNetwork,
+                        INITIAL_NETWORK_CAPABILITIES,
+                        INITIAL_LINK_PROPERTIES,
+                        false /* isBlocked */,
+                        true /* isSelected */,
+                        -1 /* priorityClass */);
+        UnderlyingNetworkRecord recordD =
+                getTestNetworkRecord(
+                        mNetwork,
                         UPDATED_NETWORK_CAPABILITIES,
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
 
         assertEquals(recordA, recordB);
-        assertNotEquals(recordA, recordC);
+        assertEquals(recordA, recordC);
+        assertNotEquals(recordA, recordD);
+
+        assertTrue(UnderlyingNetworkRecord.isEqualIncludingPriorities(recordA, recordB));
+        assertFalse(UnderlyingNetworkRecord.isEqualIncludingPriorities(recordA, recordC));
     }
 
     @Test
@@ -366,6 +555,10 @@
                 .build();
     }
 
+    private void verifyOnSelectedUnderlyingNetworkChanged(UnderlyingNetworkRecord expectedRecord) {
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback(
             NetworkCapabilities networkCapabilities) {
         verify(mConnectivityManager)
@@ -384,12 +577,12 @@
         cb.onBlockedStatusChanged(mNetwork, false /* isFalse */);
 
         UnderlyingNetworkRecord expectedRecord =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
         return cb;
     }
 
@@ -402,12 +595,12 @@
         cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
     }
 
     @Test
@@ -417,12 +610,12 @@
         cb.onLinkPropertiesChanged(mNetwork, UPDATED_LINK_PROPERTIES);
 
         UnderlyingNetworkRecord expectedRecord =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS),
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
     }
 
     @Test
@@ -434,18 +627,16 @@
         cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkControllerCb, times(1))
-                .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
         // onSelectedUnderlyingNetworkChanged() won't be fired twice if network capabilities doesn't
         // change.
         cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
-        verify(mNetworkControllerCb, times(1))
-                .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
     }
 
     @Test
@@ -458,18 +649,16 @@
         cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
-                new UnderlyingNetworkRecord(
+                getTestNetworkRecord(
                         mNetwork,
                         responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
-        verify(mNetworkControllerCb, times(1))
-                .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
         // onSelectedUnderlyingNetworkChanged() won't be fired twice if network capabilities doesn't
         // change.
         cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
-        verify(mNetworkControllerCb, times(1))
-                .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
     }
 
     @Test
@@ -478,13 +667,7 @@
 
         cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */);
 
-        UnderlyingNetworkRecord expectedRecord =
-                new UnderlyingNetworkRecord(
-                        mNetwork,
-                        buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS),
-                        INITIAL_LINK_PROPERTIES,
-                        true /* isBlocked */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
+        verifyOnSelectedUnderlyingNetworkChanged(null);
     }
 
     @Test
@@ -520,5 +703,132 @@
         verify(mNetworkControllerCb, times(1)).onSelectedUnderlyingNetworkChanged(any());
     }
 
-    // TODO (b/187991063): Add tests for network prioritization
+    private UnderlyingNetworkListener setupControllerAndGetNetworkListener(
+            List<VcnUnderlyingNetworkTemplate> networkTemplates) {
+        final ConnectivityManager cm = mock(ConnectivityManager.class);
+        setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
+
+        new UnderlyingNetworkController(
+                mVcnContext,
+                VcnGatewayConnectionConfigTest.buildTestConfig(networkTemplates),
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mNetworkControllerCb);
+
+        verify(cm)
+                .registerNetworkCallback(
+                        eq(getRouteSelectionRequest(INITIAL_SUB_IDS)),
+                        mUnderlyingNetworkListenerCaptor.capture(),
+                        any());
+
+        return mUnderlyingNetworkListenerCaptor.getValue();
+    }
+
+    private UnderlyingNetworkRecord bringupNetworkAndGetRecord(
+            UnderlyingNetworkListener cb,
+            NetworkCapabilities requestNetworkCaps,
+            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            UnderlyingNetworkRecord currentlySelected) {
+        final Network network = mock(Network.class);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(requestNetworkCaps, INITIAL_SUB_IDS);
+
+        cb.onAvailable(network);
+        cb.onCapabilitiesChanged(network, responseNetworkCaps);
+        cb.onLinkPropertiesChanged(network, INITIAL_LINK_PROPERTIES);
+        cb.onBlockedStatusChanged(network, false /* isFalse */);
+        return new UnderlyingNetworkRecord(
+                network,
+                responseNetworkCaps,
+                INITIAL_LINK_PROPERTIES,
+                false /* isBlocked */,
+                mVcnContext,
+                underlyingNetworkTemplates,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                currentlySelected,
+                null /* carrierConfig */);
+    }
+
+    @Test
+    public void testSelectMorePreferredNetwork() {
+        final List<VcnUnderlyingNetworkTemplate> networkTemplates = new ArrayList();
+        networkTemplates.add(CELL_TEMPLATE_DUN);
+        networkTemplates.add(CELL_TEMPLATE_CBS);
+
+        UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
+
+        // Bring up CBS network
+        final UnderlyingNetworkRecord cbsNetworkRecord =
+                bringupNetworkAndGetRecord(
+                        cb,
+                        CBS_NETWORK_CAPABILITIES,
+                        networkTemplates,
+                        null /* currentlySelected */);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(cbsNetworkRecord));
+
+        // Bring up DUN network
+        final UnderlyingNetworkRecord dunNetworkRecord =
+                bringupNetworkAndGetRecord(
+                        cb, DUN_NETWORK_CAPABILITIES, networkTemplates, cbsNetworkRecord);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(dunNetworkRecord));
+    }
+
+    @Test
+    public void testNeverSelectLessPreferredNetwork() {
+        final List<VcnUnderlyingNetworkTemplate> networkTemplates = new ArrayList();
+        networkTemplates.add(CELL_TEMPLATE_DUN);
+        networkTemplates.add(CELL_TEMPLATE_CBS);
+
+        UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
+
+        // Bring up DUN network
+        final UnderlyingNetworkRecord dunNetworkRecord =
+                bringupNetworkAndGetRecord(
+                        cb,
+                        DUN_NETWORK_CAPABILITIES,
+                        networkTemplates,
+                        null /* currentlySelected */);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(dunNetworkRecord));
+
+        // Bring up CBS network
+        final UnderlyingNetworkRecord cbsNetworkRecord =
+                bringupNetworkAndGetRecord(
+                        cb, CBS_NETWORK_CAPABILITIES, networkTemplates, dunNetworkRecord);
+        verify(mNetworkControllerCb, never())
+                .onSelectedUnderlyingNetworkChanged(eq(cbsNetworkRecord));
+    }
+
+    @Test
+    public void testFailtoMatchTemplateAndFallBackToInternetNetwork() {
+        final List<VcnUnderlyingNetworkTemplate> networkTemplates = new ArrayList();
+
+        networkTemplates.add(
+                new VcnCellUnderlyingNetworkTemplate.Builder().setDun(MATCH_REQUIRED).build());
+        UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
+
+        // Bring up an Internet network without DUN capability
+        final UnderlyingNetworkRecord networkRecord =
+                bringupNetworkAndGetRecord(
+                        cb,
+                        INITIAL_NETWORK_CAPABILITIES,
+                        networkTemplates,
+                        null /* currentlySelected */);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(networkRecord));
+    }
+
+    @Test
+    public void testFailtoMatchTemplateAndNeverFallBackToNonInternetNetwork() {
+        final List<VcnUnderlyingNetworkTemplate> networkTemplates = new ArrayList();
+
+        networkTemplates.add(
+                new VcnCellUnderlyingNetworkTemplate.Builder().setDun(MATCH_REQUIRED).build());
+        UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
+
+        bringupNetworkAndGetRecord(
+                cb, CBS_NETWORK_CAPABILITIES, networkTemplates, null /* currentlySelected */);
+
+        verify(mNetworkControllerCb, never())
+                .onSelectedUnderlyingNetworkChanged(any(UnderlyingNetworkRecord.class));
+    }
 }