Merge "Dropping NAT-T keepalive packet from APF"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..45884c4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/.idea
+*.iml
diff --git a/cmds/appwidget/appwidget b/cmds/appwidget/appwidget
index 26ab173..cc70ca5 100755
--- a/cmds/appwidget/appwidget
+++ b/cmds/appwidget/appwidget
@@ -1,6 +1,3 @@
 #!/system/bin/sh
-# Script to start "appwidget" on the device, which has a very rudimentary shell.
-base=/system
-export CLASSPATH=$base/framework/appwidget.jar
-exec app_process $base/bin com.android.commands.appwidget.AppWidget "$@"
-
+export CLASSPATH=/system/framework/appwidget.jar
+exec app_process /system/bin com.android.commands.appwidget.AppWidget "$@"
diff --git a/cmds/bmgr/bmgr b/cmds/bmgr/bmgr
index 60b5833..b068d10 100755
--- a/cmds/bmgr/bmgr
+++ b/cmds/bmgr/bmgr
@@ -1,8 +1,3 @@
 #!/system/bin/sh
-# Script to start "bmgr" on the device, which has a very rudimentary
-# shell.
-#
-base=/system
-export CLASSPATH=$base/framework/bmgr.jar
-exec app_process $base/bin com.android.commands.bmgr.Bmgr "$@"
-
+export CLASSPATH=/system/framework/bmgr.jar
+exec app_process /system/bin com.android.commands.bmgr.Bmgr "$@"
diff --git a/cmds/content/content b/cmds/content/content
index f1bfe17..91f2dfb 100755
--- a/cmds/content/content
+++ b/cmds/content/content
@@ -1,6 +1,3 @@
 #!/system/bin/sh
-# Script to start "content" on the device, which has a very rudimentary shell.
-base=/system
-export CLASSPATH=$base/framework/content.jar
-exec app_process $base/bin com.android.commands.content.Content "$@"
-
+export CLASSPATH=/system/framework/content.jar
+exec app_process /system/bin com.android.commands.content.Content "$@"
diff --git a/cmds/hid/hid b/cmds/hid/hid
index 3931da1..43c7634 100755
--- a/cmds/hid/hid
+++ b/cmds/hid/hid
@@ -1,14 +1,9 @@
 #!/system/bin/sh
-#
-# Script to start "hid" on the device, which has a very rudimentary
-# shell.
-#
-base=/system
-export CLASSPATH=$base/framework/hid.jar
 
 # Preload the native portion libhidcommand_jni.so to bypass the dependency
 # checks in the Java classloader, which prohibit dependencies that aren't
 # listed in system/core/rootdir/etc/public.libraries.android.txt.
 export LD_PRELOAD=libhidcommand_jni.so
 
-exec app_process $base/bin com.android.commands.hid.Hid "$@"
+export CLASSPATH=/system/framework/hid.jar
+exec app_process /system/bin com.android.commands.hid.Hid "$@"
diff --git a/cmds/input/input b/cmds/input/input
index 54ab947..2625eba 100755
--- a/cmds/input/input
+++ b/cmds/input/input
@@ -1,8 +1,3 @@
 #!/system/bin/sh
-# Script to start "input" on the device, which has a very rudimentary
-# shell.
-#
-base=/system
-export CLASSPATH=$base/framework/input.jar
-exec app_process $base/bin com.android.commands.input.Input "$@"
-
+export CLASSPATH=/system/framework/input.jar
+exec app_process /system/bin com.android.commands.input.Input "$@"
diff --git a/cmds/media/media b/cmds/media/media
index 8ada914..00c3915 100755
--- a/cmds/media/media
+++ b/cmds/media/media
@@ -1,7 +1,3 @@
 #!/system/bin/sh
-# Script to start "media_cmd" on the device, which has a very rudimentary
-# shell.
-#
-base=/system
-export CLASSPATH=$base/framework/media.jar
-exec app_process $base/bin com.android.commands.media.Media "$@"
+export CLASSPATH=/system/framework/media.jar
+exec app_process /system/bin com.android.commands.media.Media "$@"
diff --git a/cmds/sm/sm b/cmds/sm/sm
index 4bc859e0..30eae00 100755
--- a/cmds/sm/sm
+++ b/cmds/sm/sm
@@ -1,7 +1,3 @@
 #!/system/bin/sh
-# Script to start "sm" on the device, which has a very rudimentary
-# shell.
-#
-base=/system
-export CLASSPATH=$base/framework/sm.jar
-exec app_process $base/bin com.android.commands.sm.Sm "$@"
+export CLASSPATH=/system/framework/sm.jar
+exec app_process /system/bin com.android.commands.sm.Sm "$@"
diff --git a/cmds/svc/svc b/cmds/svc/svc
index 07b50fe..c122e98 100755
--- a/cmds/svc/svc
+++ b/cmds/svc/svc
@@ -1,8 +1,3 @@
 #!/system/bin/sh
-# Script to start "am" on the device, which has a very rudimentary
-# shell.
-#
-base=/system
-export CLASSPATH=$base/framework/svc.jar
-exec app_process $base/bin com.android.commands.svc.Svc $*
-
+export CLASSPATH=/system/framework/svc.jar
+exec app_process /system/bin com.android.commands.svc.Svc "$@"
diff --git a/packages/NetworkStack/src/android/net/ip/IpClient.java b/packages/NetworkStack/src/android/net/ip/IpClient.java
index dc74c04..266b1b0 100644
--- a/packages/NetworkStack/src/android/net/ip/IpClient.java
+++ b/packages/NetworkStack/src/android/net/ip/IpClient.java
@@ -372,10 +372,6 @@
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
 
-    /* This must match the definition in KeepaliveTracker.KeepaliveInfo */
-    private static final int TYPE_NATT = 1;
-    private static final int TYPE_TCP = 2;
-
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
      * Callback.startReadPacketFilter() and completed when the WiFi Service responds with an
@@ -705,7 +701,7 @@
      * keepalive offload.
      */
     public void addKeepalivePacketFilter(int slot, @NonNull TcpKeepalivePacketDataParcelable pkt) {
-        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, TYPE_TCP, pkt);
+        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, 0 /* Unused */, pkt);
     }
 
     /**
@@ -714,7 +710,7 @@
      */
     public void addNattKeepalivePacketFilter(int slot,
             @NonNull NattKeepalivePacketDataParcelable pkt) {
-        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, TYPE_NATT, pkt);
+        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, 0 /* Unused */ , pkt);
     }
 
     /**
@@ -1626,13 +1622,12 @@
 
                 case CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF: {
                     final int slot = msg.arg1;
-                    final int type = msg.arg2;
 
                     if (mApfFilter != null) {
-                        if (type == TYPE_NATT) {
+                        if (msg.obj instanceof NattKeepalivePacketDataParcelable) {
                             mApfFilter.addNattKeepalivePacketFilter(slot,
                                     (NattKeepalivePacketDataParcelable) msg.obj);
-                        } else {
+                        } else if (msg.obj instanceof TcpKeepalivePacketDataParcelable) {
                             mApfFilter.addTcpKeepalivePacketFilter(slot,
                                     (TcpKeepalivePacketDataParcelable) msg.obj);
                         }
diff --git a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
index 7bdf396..8e9350d 100644
--- a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
@@ -520,6 +520,9 @@
         return NetworkMonitorUtils.isValidationRequired(mNetworkCapabilities);
     }
 
+    private boolean isPrivateDnsValidationRequired() {
+        return NetworkMonitorUtils.isPrivateDnsValidationRequired(mNetworkCapabilities);
+    }
 
     private void notifyNetworkTested(int result, @Nullable String redirectUrl) {
         try {
@@ -607,7 +610,7 @@
                     return HANDLED;
                 case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
                     final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
-                    if (!isValidationRequired() || cfg == null || !cfg.inStrictMode()) {
+                    if (!isPrivateDnsValidationRequired() || cfg == null || !cfg.inStrictMode()) {
                         // No DNS resolution required.
                         //
                         // We don't force any validation in opportunistic mode
@@ -843,9 +846,20 @@
                     //    the network so don't bother validating here.  Furthermore sending HTTP
                     //    packets over the network may be undesirable, for example an extremely
                     //    expensive metered network, or unwanted leaking of the User Agent string.
+                    //
+                    // On networks that need to support private DNS in strict mode (e.g., VPNs, but
+                    // not networks that don't provide Internet access), we still need to perform
+                    // private DNS server resolution.
                     if (!isValidationRequired()) {
-                        validationLog("Network would not satisfy default request, not validating");
-                        transitionTo(mValidatedState);
+                        if (isPrivateDnsValidationRequired()) {
+                            validationLog("Network would not satisfy default request, "
+                                    + "resolving private DNS");
+                            transitionTo(mEvaluatingPrivateDnsState);
+                        } else {
+                            validationLog("Network would not satisfy default request, "
+                                    + "not validating");
+                            transitionTo(mValidatedState);
+                        }
                         return HANDLED;
                     }
                     mEvaluateAttempts++;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index cba823c..9e016c2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -120,6 +120,8 @@
 
     // See mConnectAttempted
     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
+    // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery
+    private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000;
     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
 
     // Active device state
@@ -245,7 +247,7 @@
             // various profiles
             // If UUIDs are not available yet, connect will be happen
             // upon arrival of the ACTION_UUID intent.
-            Log.d(TAG, "No profiles. Maybe we will connect later");
+            Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice);
             return;
         }
 
@@ -681,10 +683,12 @@
         long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
+        } else if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)) {
+            timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT;
         }
 
         if (DEBUG) {
-            Log.d(TAG, "onUuidChanged: Time since last connect"
+            Log.d(TAG, "onUuidChanged: Time since last connect="
                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
         }
 
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index d22a5d2..562199a 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -40,7 +40,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkPolicyManager.RULE_NONE;
 import static android.net.NetworkPolicyManager.uidRulesToString;
-import static android.net.shared.NetworkMonitorUtils.isValidationRequired;
+import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequired;
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -2824,8 +2824,8 @@
         }
     }
 
-    private boolean networkRequiresValidation(NetworkAgentInfo nai) {
-        return isValidationRequired(nai.networkCapabilities);
+    private boolean networkRequiresPrivateDnsValidation(NetworkAgentInfo nai) {
+        return isPrivateDnsValidationRequired(nai.networkCapabilities);
     }
 
     private void handleFreshlyValidatedNetwork(NetworkAgentInfo nai) {
@@ -2843,7 +2843,7 @@
 
         for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
             handlePerNetworkPrivateDnsConfig(nai, cfg);
-            if (networkRequiresValidation(nai)) {
+            if (networkRequiresPrivateDnsValidation(nai)) {
                 handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
             }
         }
@@ -2852,7 +2852,7 @@
     private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
         // Private DNS only ever applies to networks that might provide
         // Internet access and therefore also require validation.
-        if (!networkRequiresValidation(nai)) return;
+        if (!networkRequiresPrivateDnsValidation(nai)) return;
 
         // Notify the NetworkAgentInfo/NetworkMonitor in case NetworkMonitor needs to cancel or
         // schedule DNS resolutions. If a DNS resolution is required the
diff --git a/services/core/java/com/android/server/connectivity/ConnectivityConstants.java b/services/core/java/com/android/server/connectivity/ConnectivityConstants.java
index 6fa98b8..0fb6fec 100644
--- a/services/core/java/com/android/server/connectivity/ConnectivityConstants.java
+++ b/services/core/java/com/android/server/connectivity/ConnectivityConstants.java
@@ -29,7 +29,7 @@
     //
     // This ensures that a) the explicitly selected network is never trumped by anything else, and
     // b) the explicitly selected network is never torn down.
-    public static final int MAXIMUM_NETWORK_SCORE = 100;
+    public static final int EXPLICITLY_SELECTED_NETWORK_SCORE = 100;
     // VPNs typically have priority over other networks. Give them a score that will
     // let them win every single time.
     public static final int VPN_DEFAULT_SCORE = 101;
diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
index 40aec8f8..e10d737 100644
--- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
+++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
@@ -327,6 +327,8 @@
                 Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.name());
                 switch (mType) {
                     case TYPE_NATT:
+                        mNai.asyncChannel.sendMessage(
+                                CMD_ADD_KEEPALIVE_PACKET_FILTER, slot, 0 /* Unused */, mPacket);
                         mNai.asyncChannel
                                 .sendMessage(CMD_START_SOCKET_KEEPALIVE, slot, mInterval, mPacket);
                         break;
@@ -337,9 +339,8 @@
                             handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET);
                             return;
                         }
-                        mNai.asyncChannel
-                                .sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, slot, 0 /* Unused */,
-                                        mPacket);
+                        mNai.asyncChannel.sendMessage(
+                                CMD_ADD_KEEPALIVE_PACKET_FILTER, slot, 0 /* Unused */, mPacket);
                         // TODO: check result from apf and notify of failure as needed.
                         mNai.asyncChannel
                                 .sendMessage(CMD_START_SOCKET_KEEPALIVE, slot, mInterval, mPacket);
@@ -375,14 +376,17 @@
                     return;
                 default:
                     mStartedState = STOPPING;
-                    if (mType == TYPE_NATT) {
-                        mNai.asyncChannel.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, mSlot);
-                    } else if (mType == TYPE_TCP) {
-                        mNai.asyncChannel.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, mSlot);
-                        mNai.asyncChannel.sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, mSlot);
-                        mTcpController.stopSocketMonitor(mSlot);
-                    } else {
-                        Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
+                    switch (mType) {
+                        case TYPE_TCP:
+                            mTcpController.stopSocketMonitor(mSlot);
+                            // fall through
+                        case TYPE_NATT:
+                            mNai.asyncChannel.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, mSlot);
+                            mNai.asyncChannel.sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER,
+                                    mSlot);
+                            break;
+                        default:
+                            Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
                     }
             }
 
@@ -458,9 +462,10 @@
     }
 
     public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
-        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
         if (networkKeepalives != null) {
-            for (KeepaliveInfo ki : networkKeepalives.values()) {
+            final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values());
+            for (KeepaliveInfo ki : kalist) {
                 ki.stop(reason);
                 // Clean up keepalives since the network agent is disconnected and unable to pass
                 // back asynchronous result of stop().
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index cfa9131..34772d0 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -483,11 +483,11 @@
         // down an explicitly selected network before the user gets a chance to prefer it when
         // a higher-scoring network (e.g., Ethernet) is available.
         if (networkMisc.explicitlySelected && (networkMisc.acceptUnvalidated || pretendValidated)) {
-            return ConnectivityConstants.MAXIMUM_NETWORK_SCORE;
+            return ConnectivityConstants.EXPLICITLY_SELECTED_NETWORK_SCORE;
         }
 
         int score = currentScore;
-        if (!lastValidated && !pretendValidated && !ignoreWifiUnvalidationPenalty()) {
+        if (!lastValidated && !pretendValidated && !ignoreWifiUnvalidationPenalty() && !isVPN()) {
             score -= ConnectivityConstants.UNVALIDATED_SCORE_PENALTY;
         }
         if (score < 0) score = 0;
diff --git a/services/net/java/android/net/shared/NetworkMonitorUtils.java b/services/net/java/android/net/shared/NetworkMonitorUtils.java
index bb4a603..46e9c73 100644
--- a/services/net/java/android/net/shared/NetworkMonitorUtils.java
+++ b/services/net/java/android/net/shared/NetworkMonitorUtils.java
@@ -43,16 +43,23 @@
             "android.permission.ACCESS_NETWORK_CONDITIONS";
 
     /**
-     * Return whether validation is required for a network.
-     * @param dfltNetCap Default requested network capabilities.
+     * Return whether validation is required for private DNS in strict mode.
      * @param nc Network capabilities of the network to test.
      */
-    public static boolean isValidationRequired(NetworkCapabilities nc) {
+    public static boolean isPrivateDnsValidationRequired(NetworkCapabilities nc) {
         // TODO: Consider requiring validation for DUN networks.
         return nc != null
                 && nc.hasCapability(NET_CAPABILITY_INTERNET)
                 && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                && nc.hasCapability(NET_CAPABILITY_TRUSTED)
-                && nc.hasCapability(NET_CAPABILITY_NOT_VPN);
+                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
+    }
+
+    /**
+     * Return whether validation is required for a network.
+     * @param nc Network capabilities of the network to test.
+     */
+    public static boolean isValidationRequired(NetworkCapabilities nc) {
+        // TODO: Consider requiring validation for DUN networks.
+        return isPrivateDnsValidationRequired(nc) && nc.hasCapability(NET_CAPABILITY_NOT_VPN);
     }
 }
diff --git a/tests/net/Android.bp b/tests/net/Android.bp
index 3fe867b..1fbb658 100644
--- a/tests/net/Android.bp
+++ b/tests/net/Android.bp
@@ -29,6 +29,7 @@
         "libbpf",
         "libbpf_android",
         "libc++",
+        "libcgrouprc",
         "libcrypto",
         "libcutils",
         "libdexfile",
diff --git a/tests/net/common/java/android/net/apf/ApfCapabilitiesTest.java b/tests/net/common/java/android/net/apf/ApfCapabilitiesTest.java
index 3ed8a86..0ce7c91 100644
--- a/tests/net/common/java/android/net/apf/ApfCapabilitiesTest.java
+++ b/tests/net/common/java/android/net/apf/ApfCapabilitiesTest.java
@@ -17,7 +17,9 @@
 package android.net.apf;
 
 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 androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -32,8 +34,12 @@
 @SmallTest
 public class ApfCapabilitiesTest {
     @Test
-    public void testParcelUnparcel() {
+    public void testConstructAndParcel() {
         final ApfCapabilities caps = new ApfCapabilities(123, 456, 789);
+        assertEquals(123, caps.apfVersionSupported);
+        assertEquals(456, caps.maximumApfProgramSize);
+        assertEquals(789, caps.apfPacketFormat);
+
         ParcelableTestUtil.assertFieldCountEquals(3, ApfCapabilities.class);
 
         TestUtils.assertParcelingIsLossless(caps);
@@ -46,4 +52,14 @@
         assertNotEquals(new ApfCapabilities(1, 3, 3), new ApfCapabilities(1, 2, 3));
         assertNotEquals(new ApfCapabilities(1, 2, 4), new ApfCapabilities(1, 2, 3));
     }
+
+    @Test
+    public void testHasDataAccess() {
+        //hasDataAccess is only supported starting at apf version 4.
+        ApfCapabilities caps = new ApfCapabilities(1 /* apfVersionSupported */, 2, 3);
+        assertFalse(caps.hasDataAccess());
+
+        caps = new ApfCapabilities(4 /* apfVersionSupported */, 5, 6);
+        assertTrue(caps.hasDataAccess());
+    }
 }
diff --git a/tests/net/java/android/net/IpMemoryStoreTest.java b/tests/net/java/android/net/IpMemoryStoreTest.java
index 8ff2de9..6e69b34 100644
--- a/tests/net/java/android/net/IpMemoryStoreTest.java
+++ b/tests/net/java/android/net/IpMemoryStoreTest.java
@@ -111,13 +111,12 @@
 
     @Test
     public void testNetworkAttributes() throws Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final String l2Key = "fakeKey";
 
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         verify(mMockService, times(1)).storeNetworkAttributes(eq(l2Key),
                 mNapCaptor.capture(), any());
         assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
@@ -135,7 +134,7 @@
 
     @Test
     public void testPrivateData() throws RemoteException {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final Blob b = new Blob();
         b.data = TEST_BLOB_DATA;
         final String l2Key = "fakeKey";
@@ -162,7 +161,7 @@
     @Test
     public void testFindL2Key()
             throws UnknownHostException, RemoteException, Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final String l2Key = "fakeKey";
 
         mStore.findL2Key(TEST_NETWORK_ATTRIBUTES,
@@ -177,7 +176,7 @@
 
     @Test
     public void testIsSameNetwork() throws UnknownHostException, RemoteException {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         final String l2Key1 = "fakeKey1";
         final String l2Key2 = "fakeKey2";
 
@@ -193,7 +192,7 @@
 
     @Test
     public void testEnqueuedIpMsRequests() throws Exception {
-        startIpMemoryStore(false);
+        startIpMemoryStore(false /* supplyService */);
 
         final Blob b = new Blob();
         b.data = TEST_BLOB_DATA;
@@ -201,9 +200,8 @@
 
         // enqueue multiple ipms requests
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveNetworkAttributes(l2Key,
                 (status, key, attr) -> {
                     assertTrue("Retrieve network attributes not successful : "
@@ -212,9 +210,8 @@
                     assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
                 });
         mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
                 (status, key, name, data) -> {
                     assertTrue("Retrieve blob status not successful : " + status.resultCode,
@@ -240,7 +237,7 @@
 
     @Test
     public void testEnqueuedIpMsRequestsWithException() throws Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
         doThrow(RemoteException.class).when(mMockService).retrieveNetworkAttributes(any(), any());
 
         final Blob b = new Blob();
@@ -249,9 +246,8 @@
 
         // enqueue multiple ipms requests
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveNetworkAttributes(l2Key,
                 (status, key, attr) -> {
                     assertTrue("Retrieve network attributes not successful : "
@@ -260,9 +256,8 @@
                     assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
                 });
         mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
                 (status, key, name, data) -> {
                     assertTrue("Retrieve blob status not successful : " + status.resultCode,
@@ -286,7 +281,7 @@
 
     @Test
     public void testEnqueuedIpMsRequestsCallbackFunctionWithException() throws Exception {
-        startIpMemoryStore(true);
+        startIpMemoryStore(true /* supplyService */);
 
         final Blob b = new Blob();
         b.data = TEST_BLOB_DATA;
@@ -294,9 +289,8 @@
 
         // enqueue multiple ipms requests
         mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
-                status -> {
-                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
-                });
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
         mStore.retrieveNetworkAttributes(l2Key,
                 (status, key, attr) -> {
                     throw new RuntimeException("retrieveNetworkAttributes test");
@@ -320,6 +314,7 @@
 
         inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(),
                 any());
+        inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any());
         inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME),
                 eq(b), any());
         inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID),
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 3a28aca..23cfbd4 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -28,6 +28,7 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
 import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
 import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
 import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY;
@@ -489,7 +490,7 @@
 
         MockNetworkAgent(int transport, LinkProperties linkProperties) {
             final int type = transportToLegacyType(transport);
-            final String typeName = ConnectivityManager.getNetworkTypeName(transport);
+            final String typeName = ConnectivityManager.getNetworkTypeName(type);
             mNetworkInfo = new NetworkInfo(type, 0, typeName, "Mock");
             mNetworkCapabilities = new NetworkCapabilities();
             mNetworkCapabilities.addTransportType(transport);
@@ -619,6 +620,10 @@
             mNetworkAgent.sendNetworkScore(mScore);
         }
 
+        public int getScore() {
+            return mScore;
+        }
+
         public void explicitlySelected(boolean acceptUnvalidated) {
             mNetworkAgent.explicitlySelected(acceptUnvalidated);
         }
@@ -1330,6 +1335,8 @@
                 return TYPE_WIFI;
             case TRANSPORT_CELLULAR:
                 return TYPE_MOBILE;
+            case TRANSPORT_VPN:
+                return TYPE_VPN;
             default:
                 return TYPE_NONE;
         }
@@ -5370,6 +5377,58 @@
     }
 
     @Test
+    public void testVpnUnvalidated() throws Exception {
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+
+        // Bring up Ethernet.
+        mEthernetNetworkAgent = new MockNetworkAgent(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+        callback.assertNoCallback();
+
+        // Bring up a VPN that has the INTERNET capability, initially unvalidated.
+        final int uid = Process.myUid();
+        final MockNetworkAgent vpnNetworkAgent = new MockNetworkAgent(TRANSPORT_VPN);
+        final ArraySet<UidRange> ranges = new ArraySet<>();
+        ranges.add(new UidRange(uid, uid));
+        mMockVpn.setNetworkAgent(vpnNetworkAgent);
+        mMockVpn.setUids(ranges);
+        vpnNetworkAgent.connect(false /* validated */, true /* hasInternet */);
+        mMockVpn.connect();
+
+        // Even though the VPN is unvalidated, it becomes the default network for our app.
+        callback.expectAvailableCallbacksUnvalidated(vpnNetworkAgent);
+        // TODO: this looks like a spurious callback.
+        callback.expectCallback(CallbackState.NETWORK_CAPABILITIES, vpnNetworkAgent);
+        callback.assertNoCallback();
+
+        assertTrue(vpnNetworkAgent.getScore() > mEthernetNetworkAgent.getScore());
+        assertEquals(ConnectivityConstants.VPN_DEFAULT_SCORE, vpnNetworkAgent.getScore());
+        assertEquals(vpnNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(vpnNetworkAgent.getNetwork());
+        assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED));
+        assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET));
+
+        assertFalse(NetworkMonitorUtils.isValidationRequired(vpnNetworkAgent.mNetworkCapabilities));
+        assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired(
+                vpnNetworkAgent.mNetworkCapabilities));
+
+        // Pretend that the VPN network validates.
+        vpnNetworkAgent.setNetworkValid();
+        vpnNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        // Expect to see the validated capability, but no other changes, because the VPN is already
+        // the default network for the app.
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, vpnNetworkAgent);
+        callback.assertNoCallback();
+
+        vpnNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, vpnNetworkAgent);
+        callback.expectAvailableCallbacksValidated(mEthernetNetworkAgent);
+    }
+
+    @Test
     public void testVpnSetUnderlyingNetworks() {
         final int uid = Process.myUid();