Merge "Allow VPN network agent to exclude local traffic"
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index ad8396b..93fc379 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -232,6 +232,20 @@
         return mLegacyExtraInfo;
     }
 
+    /**
+     * If the {@link Network} is a VPN, whether the local traffic is exempted from the VPN.
+     * @hide
+     */
+    public boolean excludeLocalRouteVpn = false;
+
+    /**
+     * @return whether local traffic is excluded from the VPN network.
+     * @hide
+     */
+    public boolean getExcludeLocalRouteVpn() {
+        return excludeLocalRouteVpn;
+    }
+
     /** @hide */
     public NetworkAgentConfig() {
     }
@@ -251,6 +265,7 @@
             legacySubType = nac.legacySubType;
             legacySubTypeName = nac.legacySubTypeName;
             mLegacyExtraInfo = nac.mLegacyExtraInfo;
+            excludeLocalRouteVpn = nac.excludeLocalRouteVpn;
         }
     }
 
@@ -407,6 +422,17 @@
         }
 
         /**
+         * Sets whether the local traffic is exempted from VPN.
+         *
+         * @return this builder, to facilitate chaining.
+         * @hide TODO(184750836): Unhide once the implementation is completed.
+         */
+        public Builder setExcludeLocalRoutesVpn(boolean excludeLocalRoutes) {
+            mConfig.excludeLocalRouteVpn = excludeLocalRoutes;
+            return this;
+        }
+
+        /**
          * Returns the constructed {@link NetworkAgentConfig} object.
          */
         @NonNull
@@ -429,14 +455,15 @@
                 && legacyType == that.legacyType
                 && Objects.equals(subscriberId, that.subscriberId)
                 && Objects.equals(legacyTypeName, that.legacyTypeName)
-                && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo);
+                && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo)
+                && excludeLocalRouteVpn == that.excludeLocalRouteVpn;
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated,
                 acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId,
-                skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo);
+                skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo, excludeLocalRouteVpn);
     }
 
     @Override
@@ -453,6 +480,7 @@
                 + ", hasShownBroken = " + hasShownBroken
                 + ", legacyTypeName = '" + legacyTypeName + '\''
                 + ", legacyExtraInfo = '" + mLegacyExtraInfo + '\''
+                + ", excludeLocalRouteVpn = '" + excludeLocalRouteVpn + '\''
                 + "}";
     }
 
@@ -475,6 +503,7 @@
         out.writeInt(legacySubType);
         out.writeString(legacySubTypeName);
         out.writeString(mLegacyExtraInfo);
+        out.writeInt(excludeLocalRouteVpn ? 1 : 0);
     }
 
     public static final @NonNull Creator<NetworkAgentConfig> CREATOR =
@@ -494,6 +523,7 @@
             networkAgentConfig.legacySubType = in.readInt();
             networkAgentConfig.legacySubTypeName = in.readString();
             networkAgentConfig.mLegacyExtraInfo = in.readString();
+            networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
             return networkAgentConfig;
         }
 
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index afaae1c..ed9995c 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -48,9 +48,16 @@
                 setBypassableVpn(true)
             }
         }.build()
+        // This test can be run as unit test against the latest system image, as CTS to verify
+        // an Android release that is as recent as the test, or as MTS to verify the
+        // Connectivity module. In the first two cases NetworkAgentConfig will be as recent
+        // as the test. In the last case, starting from S NetworkAgentConfig is updated as part
+        // of Connectivity, so it is also as recent as the test. For MTS on Q and R,
+        // NetworkAgentConfig is not updatable, so it may have a different number of fields.
         if (isAtLeastS()) {
-            // From S, the config will have 12 items
-            assertParcelSane(config, 12)
+            // When this test is run on S+, NetworkAgentConfig is as recent as the test,
+            // so this should be the most recent known number of fields.
+            assertParcelSane(config, 13)
         } else {
             // For R or below, the config will have 10 items
             assertParcelSane(config, 10)
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index a945a1f..960a9f1 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.net;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.ParcelUtils.assertParcelSane;
 
 import static org.junit.Assert.assertEquals;
@@ -48,6 +49,7 @@
 
     private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
     private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
+    private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25;
 
     @Test
     public void testDefaults() throws Exception {
@@ -126,7 +128,12 @@
 
     @Test
     public void testParcelUnparcel() {
-        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
+        if (isAtLeastT()) {
+            // excludeLocalRoutes is added in T.
+            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 24);
+        } else {
+            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
+        }
     }
 
     @Test
@@ -166,7 +173,8 @@
         final String tooFewValues =
                 getEncodedDecodedIkev2ProfileMissingValues(
                         ENCODED_INDEX_AUTH_PARAMS_INLINE,
-                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */);
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
+                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE /* missingIndices */);
 
         assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
     }
@@ -183,6 +191,17 @@
     }
 
     @Test
+    public void testEncodeDecodeMissingExcludeLocalRoutes() {
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE /* missingIndices */);
+
+        // Verify decoding without isRestrictedToTestNetworks defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.excludeLocalRoutes);
+    }
+
+    @Test
     public void testEncodeDecodeLoginsNotSaved() {
         final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
         profile.saveLogin = false;