Merge "Adding fuzzer ResXMLTree" into main
diff --git a/PACKAGE_MANAGER_OWNERS b/PACKAGE_MANAGER_OWNERS
index eb5842b..45719a7 100644
--- a/PACKAGE_MANAGER_OWNERS
+++ b/PACKAGE_MANAGER_OWNERS
@@ -1,3 +1,6 @@
+# Bug component: 36137
+# Bug template url: https://b.corp.google.com/issues/new?component=36137&template=198919
+
 alexbuy@google.com
 patb@google.com
 schfan@google.com
\ No newline at end of file
diff --git a/api/Android.bp b/api/Android.bp
index bc217f3..f049ed9 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -306,6 +306,30 @@
     "org.xmlpull",
 ]
 
+// These are libs from framework-internal-utils that are required (i.e. being referenced)
+// from framework-non-updatable-sources. Add more here when there's a need.
+// DO NOT add the entire framework-internal-utils. It might cause unnecessary circular
+// dependencies gets bigger.
+android_non_updatable_stubs_libs = [
+    "android.hardware.cas-V1.2-java",
+    "android.hardware.health-V1.0-java-constants",
+    "android.hardware.radio-V1.5-java",
+    "android.hardware.radio-V1.6-java",
+    "android.hardware.thermal-V1.0-java-constants",
+    "android.hardware.thermal-V2.0-java",
+    "android.hardware.tv.input-V1.0-java-constants",
+    "android.hardware.usb-V1.0-java-constants",
+    "android.hardware.usb-V1.1-java-constants",
+    "android.hardware.usb.gadget-V1.0-java",
+    "android.hardware.vibrator-V1.3-java",
+    "framework-protos",
+]
+
+java_defaults {
+    name: "android-non-updatable-stubs-libs-defaults",
+    libs: android_non_updatable_stubs_libs,
+}
+
 // Defaults for all stubs that include the non-updatable framework. These defaults do not include
 // module symbols, so will not compile correctly on their own. Users must add module APIs to the
 // classpath (or sources) somehow.
@@ -333,24 +357,7 @@
             "system/hardware/interfaces/media/aidl",
         ],
     },
-    // These are libs from framework-internal-utils that are required (i.e. being referenced)
-    // from framework-non-updatable-sources. Add more here when there's a need.
-    // DO NOT add the entire framework-internal-utils. It might cause unnecessary circular
-    // dependencies gets bigger.
-    libs: [
-        "android.hardware.cas-V1.2-java",
-        "android.hardware.health-V1.0-java-constants",
-        "android.hardware.radio-V1.5-java",
-        "android.hardware.radio-V1.6-java",
-        "android.hardware.thermal-V1.0-java-constants",
-        "android.hardware.thermal-V2.0-java",
-        "android.hardware.tv.input-V1.0-java-constants",
-        "android.hardware.usb-V1.0-java-constants",
-        "android.hardware.usb-V1.1-java-constants",
-        "android.hardware.usb.gadget-V1.0-java",
-        "android.hardware.vibrator-V1.3-java",
-        "framework-protos",
-    ],
+    libs: android_non_updatable_stubs_libs,
     flags: [
         "--error NoSettingsProvider",
         "--error UnhiddenSystemApi",
diff --git a/media/java/android/media/metrics/PlaybackSession.java b/media/java/android/media/metrics/PlaybackSession.java
index f8dd756..6223acf 100644
--- a/media/java/android/media/metrics/PlaybackSession.java
+++ b/media/java/android/media/metrics/PlaybackSession.java
@@ -24,7 +24,10 @@
 import java.util.Objects;
 
 /**
- * An instances of this class represents a session of media playback.
+ * An instance of this class represents a session of media playback used to report playback
+ * metrics and events.
+ *
+ * Create a new instance using {@link MediaMetricsManager#createPlaybackSession}.
  */
 public final class PlaybackSession implements AutoCloseable {
     private final @NonNull String mId;
@@ -80,6 +83,21 @@
         mManager.reportTrackChangeEvent(mId, event);
     }
 
+    /**
+     * A session ID is used to identify a unique playback and to tie together lower-level
+     * playback components.
+     *
+     * Associate this session with a {@link MediaCodec} by passing the ID into
+     * {@link MediaFormat} through {@link MediaFormat#LOG_SESSION_ID} when
+     * creating the {@link MediaCodec}.
+     *
+     * Associate this session with an {@link AudioTrack} by calling
+     * {@link AudioTrack#setLogSessionId}.
+     *
+     * Associate this session with {@link MediaDrm} and {@link MediaCrypto} by calling
+     * {@link MediaDrm#getPlaybackComponent} and then calling
+     * {@link PlaybackComponent#setLogSessionId}.
+     */
     public @NonNull LogSessionId getSessionId() {
         return mLogSessionId;
     }
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 8396005..0fc80dd 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2895,6 +2895,10 @@
         jint offset,
         jint size,
         std::shared_ptr<C2Buffer> *buffer) {
+    if ((offset + size) > context->capacity()) {
+        ALOGW("extractBufferFromContext: offset + size provided exceed capacity");
+        return;
+    }
     *buffer = context->toC2Buffer(offset, size);
     if (*buffer == nullptr) {
         if (!context->mMemory) {
@@ -2995,18 +2999,15 @@
                     "MediaCodec.LinearBlock#obtain method to obtain a compatible buffer.");
             return;
         }
-        sp<CryptoInfosWrapper> cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()};
-        jint sampleSize = 0;
+        sp<CryptoInfosWrapper> cryptoInfos = nullptr;
+        jint sampleSize = totalSize;
         if (cryptoInfoArray != nullptr) {
+            cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()};
             extractCryptoInfosFromObjectArray(env,
                     &sampleSize,
                     &cryptoInfos->value,
                     cryptoInfoArray,
                     &errorDetailMsg);
-        } else {
-            sampleSize = totalSize;
-            std::unique_ptr<CodecCryptoInfo> cryptoInfo{new MediaCodecCryptoInfo(totalSize)};
-            cryptoInfos->value.push_back(std::move(cryptoInfo));
         }
         if (env->ExceptionCheck()) {
             // Creation of cryptoInfo failed. Let the exception bubble up.
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index c26fd5d..3ab9780 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -264,6 +264,11 @@
                 && record.getDeliveryGroupPolicy() == BroadcastOptions.DELIVERY_GROUP_POLICY_ALL) {
             final BroadcastRecord replacedBroadcastRecord = replaceBroadcast(record, recordIndex);
             if (replacedBroadcastRecord != null) {
+                if (mLastDeferredStates && shouldBeDeferred()
+                        && (record.getDeliveryState(recordIndex)
+                                == BroadcastRecord.DELIVERY_PENDING)) {
+                    deferredStatesApplyConsumer.accept(record, recordIndex);
+                }
                 return replacedBroadcastRecord;
             }
         }
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index d584c99..d4e46a9 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -179,15 +179,18 @@
             nextConsumer.consume(current);
         } else if (mNextConsumer != null) {
             mNextConsumer.add(nextConsumer);
-        } else {
+        } else if (mLightSensor != null) {
             mDestroyed = false;
             mNextConsumer = nextConsumer;
             enableLightSensorLoggingLocked();
+        } else {
+            Slog.w(TAG, "No light sensor - use current to consume");
+            nextConsumer.consume(current);
         }
     }
 
     private void enableLightSensorLoggingLocked() {
-        if (!mEnabled) {
+        if (!mEnabled && mLightSensor != null) {
             mEnabled = true;
             mLastAmbientLux = -1;
             mSensorManager.registerListener(mLightSensorListener, mLightSensor,
@@ -201,7 +204,7 @@
     private void disableLightSensorLoggingLocked(boolean destroying) {
         resetTimerLocked(false /* start */);
 
-        if (mEnabled) {
+        if (mEnabled && mLightSensor != null) {
             mEnabled = false;
             if (!destroying) {
                 mLastAmbientLux = -1;
diff --git a/services/core/java/com/android/server/net/TEST_MAPPING b/services/core/java/com/android/server/net/TEST_MAPPING
index e0376ed..8db5905 100644
--- a/services/core/java/com/android/server/net/TEST_MAPPING
+++ b/services/core/java/com/android/server/net/TEST_MAPPING
@@ -28,5 +28,10 @@
         }
       ]
     }
+  ],
+  "postsubmit":[
+    {
+      "name":"FrameworksVpnTests"
+    }
   ]
 }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 677a5d1..e2f8c50 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -3194,12 +3194,7 @@
 
                     } else if (tagName.equals("verifier")) {
                         final String deviceIdentity = parser.getAttributeValue(null, "device");
-                        try {
-                            mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);
-                        } catch (IllegalArgumentException e) {
-                            Slog.w(PackageManagerService.TAG, "Discard invalid verifier device id: "
-                                    + e.getMessage());
-                        }
+                        mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);
                     } else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {
                         // No longer used.
                     } else if (tagName.equals("keyset-settings")) {
@@ -3228,7 +3223,7 @@
                 }
 
                 str.close();
-            } catch (IOException | XmlPullParserException e) {
+            } catch (IOException | XmlPullParserException | IllegalArgumentException e) {
                 // Remove corrupted file and retry.
                 atomicFile.failRead(str, e);
 
diff --git a/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
new file mode 100644
index 0000000..180f54e
--- /dev/null
+++ b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
+import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.ipsec.ike.IkeKeyIdIdentification;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.net.VpnProfile;
+import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
+import com.android.net.module.util.ProxyUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.security.auth.x500.X500Principal;
+
+/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class Ikev2VpnProfileTest {
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final String USERNAME_STRING = "username";
+    private static final String PASSWORD_STRING = "pa55w0rd";
+    private static final String EXCL_LIST = "exclList";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
+    private static final int TEST_MTU = 1300;
+
+    private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
+            SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
+
+    private X509Certificate mUserCert;
+    private X509Certificate mServerRootCa;
+    private PrivateKey mPrivateKey;
+
+    @Before
+    public void setUp() throws Exception {
+        mServerRootCa = generateRandomCertAndKeyPair().cert;
+
+        final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
+        mUserCert = userCertKey.cert;
+        mPrivateKey = userCertKey.key;
+    }
+
+    private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);
+
+        builder.setBypassable(true);
+        builder.setProxy(mProxy);
+        builder.setMaxMtu(TEST_MTU);
+        builder.setMetered(true);
+
+        return builder;
+    }
+
+    @Test
+    public void testBuildValidProfileWithOptions() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        // Check non-auth parameters correctly stored
+        assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
+        assertEquals(IDENTITY_STRING, profile.getUserIdentity());
+        assertEquals(mProxy, profile.getProxyInfo());
+        assertTrue(profile.isBypassable());
+        assertTrue(profile.isMetered());
+        assertEquals(TEST_MTU, profile.getMaxMtu());
+        assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testBuildUsernamePasswordProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertEquals(USERNAME_STRING, profile.getUsername());
+        assertEquals(PASSWORD_STRING, profile.getPassword());
+        assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+        assertNull(profile.getPresharedKey());
+        assertNull(profile.getRsaPrivateKey());
+        assertNull(profile.getUserCert());
+    }
+
+    @Test
+    public void testBuildDigitalSignatureProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertEquals(profile.getUserCert(), mUserCert);
+        assertEquals(mPrivateKey, profile.getRsaPrivateKey());
+        assertEquals(profile.getServerRootCaCert(), mServerRootCa);
+
+        assertNull(profile.getPresharedKey());
+        assertNull(profile.getUsername());
+        assertNull(profile.getPassword());
+    }
+
+    @Test
+    public void testBuildPresharedKeyProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertArrayEquals(PSK_BYTES, profile.getPresharedKey());
+
+        assertNull(profile.getServerRootCaCert());
+        assertNull(profile.getUsername());
+        assertNull(profile.getPassword());
+        assertNull(profile.getRsaPrivateKey());
+        assertNull(profile.getUserCert());
+    }
+
+    @Test
+    public void testBuildWithAllowedAlgorithmsAead() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+
+        List<String> allowedAlgorithms =
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305);
+        builder.setAllowedAlgorithms(allowedAlgorithms);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testBuildWithAllowedAlgorithmsNormal() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+
+        List<String> allowedAlgorithms =
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.AUTH_AES_XCBC,
+                        IpSecAlgorithm.AUTH_AES_CMAC,
+                        IpSecAlgorithm.CRYPT_AES_CBC,
+                        IpSecAlgorithm.CRYPT_AES_CTR);
+        builder.setAllowedAlgorithms(allowedAlgorithms);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsEmptyList() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(new ArrayList<>());
+            fail("Expected exception due to no valid algorithm set");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsInvalidList() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
+            fail("Expected exception due to missing encryption");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC));
+            fail("Expected exception due to missing authentication");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
+            fail("Expected exception due to insecure algorithm");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1));
+            fail("Expected exception due to insecure algorithm");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildNoAuthMethodSet() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.build();
+            fail("Expected exception due to lack of auth method");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildExcludeLocalRoutesSet() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+        builder.setLocalRoutesExcluded(true);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+        assertTrue(profile.areLocalRoutesExcluded());
+
+        builder.setBypassable(false);
+        try {
+            builder.build();
+            fail("Expected exception because excludeLocalRoutes should be set only"
+                    + " on the bypassable VPN");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildInvalidMtu() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setMaxMtu(500);
+            fail("Expected exception due to too-small MTU");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private void verifyVpnProfileCommon(VpnProfile profile) {
+        assertEquals(SERVER_ADDR_STRING, profile.server);
+        assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
+        assertEquals(mProxy, profile.proxy);
+        assertTrue(profile.isBypassable);
+        assertTrue(profile.isMetered);
+        assertEquals(TEST_MTU, profile.maxMtu);
+    }
+
+    @Test
+    public void testPskConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        verifyVpnProfileCommon(profile);
+        assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);
+
+        // Check nothing else is set
+        assertEquals("", profile.username);
+        assertEquals("", profile.password);
+        assertEquals("", profile.ipsecUserCert);
+        assertEquals("", profile.ipsecCaCert);
+    }
+
+    @Test
+    public void testUsernamePasswordConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        verifyVpnProfileCommon(profile);
+        assertEquals(USERNAME_STRING, profile.username);
+        assertEquals(PASSWORD_STRING, profile.password);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
+
+        // Check nothing else is set
+        assertEquals("", profile.ipsecUserCert);
+        assertEquals("", profile.ipsecSecret);
+    }
+
+    @Test
+    public void testRsaConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
+                + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
+        verifyVpnProfileCommon(profile);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
+        assertEquals(
+                expectedSecret,
+                profile.ipsecSecret);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
+
+        // Check nothing else is set
+        assertEquals("", profile.username);
+        assertEquals("", profile.password);
+    }
+
+    @Test
+    public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.username = USERNAME_STRING;
+        profile.password = PASSWORD_STRING;
+        profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
+        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getUsername());
+        assertNull(result.getPassword());
+        assertNull(result.getUserCert());
+        assertNull(result.getRsaPrivateKey());
+        assertNull(result.getServerRootCaCert());
+    }
+
+    @Test
+    public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.ipsecSecret = new String(PSK_BYTES);
+        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getPresharedKey());
+        assertNull(result.getUserCert());
+        assertNull(result.getRsaPrivateKey());
+    }
+
+    @Test
+    public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.username = USERNAME_STRING;
+        profile.password = PASSWORD_STRING;
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getUsername());
+        assertNull(result.getPassword());
+        assertNull(result.getPresharedKey());
+    }
+
+    @Test
+    public void testPskConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testUsernamePasswordConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testRsaConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testBuildWithIkeTunConnParamsConvertToVpnProfile() throws Exception {
+        // Special keyId that contains delimiter character of VpnProfile
+        final byte[] keyId = "foo\0bar".getBytes();
+        final IkeTunnelConnectionParams tunnelParams = new IkeTunnelConnectionParams(
+                getTestIkeSessionParams(true /* testIpv6 */, new IkeKeyIdIdentification(keyId)),
+                CHILD_PARAMS);
+        final Ikev2VpnProfile ikev2VpnProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
+        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
+
+        assertEquals(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS, vpnProfile.type);
+
+        // Username, password, server, ipsecIdentifier, ipsecCaCert, ipsecSecret, ipsecUserCert and
+        // getAllowedAlgorithms should not be set if IkeTunnelConnectionParams is set.
+        assertEquals("", vpnProfile.server);
+        assertEquals("", vpnProfile.ipsecIdentifier);
+        assertEquals("", vpnProfile.username);
+        assertEquals("", vpnProfile.password);
+        assertEquals("", vpnProfile.ipsecCaCert);
+        assertEquals("", vpnProfile.ipsecSecret);
+        assertEquals("", vpnProfile.ipsecUserCert);
+        assertEquals(0, vpnProfile.getAllowedAlgorithms().size());
+
+        // IkeTunnelConnectionParams should stay the same.
+        assertEquals(tunnelParams, vpnProfile.ikeTunConnParams);
+
+        // Convert to disk-stable format and then back to Ikev2VpnProfile should be the same.
+        final VpnProfile decodedVpnProfile =
+                VpnProfile.decode(vpnProfile.key, vpnProfile.encode());
+        final Ikev2VpnProfile convertedIkev2VpnProfile =
+                Ikev2VpnProfile.fromVpnProfile(decodedVpnProfile);
+        assertEquals(ikev2VpnProfile, convertedIkev2VpnProfile);
+    }
+
+    @Test
+    public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
+        // Config authentication related fields is not required while building with
+        // IkeTunnelConnectionParams.
+        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAutomaticNattKeepaliveTimerEnabled(true);
+        builder.setAutomaticIpVersionSelectionEnabled(true);
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testAutomaticNattAndIpVersionDefaults() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled());
+        assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled());
+    }
+
+    @Test
+    public void testEquals() throws Exception {
+        // Verify building without IkeTunnelConnectionParams
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        assertEquals(builder.build(), builder.build());
+
+        // Verify building with IkeTunnelConnectionParams
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
+        final IkeTunnelConnectionParams tunnelParams2 =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
+        assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
+                new Ikev2VpnProfile.Builder(tunnelParams2).build());
+    }
+
+    @Test
+    public void testBuildProfileWithNullProxy() throws Exception {
+        final Ikev2VpnProfile ikev2VpnProfile =
+                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                        .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa)
+                        .build();
+
+        // ProxyInfo should be null for the profile without setting ProxyInfo.
+        assertNull(ikev2VpnProfile.getProxyInfo());
+
+        // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile()
+        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
+        assertNull(vpnProfile.proxy);
+
+        final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile);
+        assertNull(convertedIkev2VpnProfile.getProxyInfo());
+    }
+
+    private static class CertificateAndKey {
+        public final X509Certificate cert;
+        public final PrivateKey key;
+
+        CertificateAndKey(X509Certificate cert, PrivateKey key) {
+            this.cert = cert;
+            this.key = key;
+        }
+    }
+
+    private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
+        final Date validityBeginDate =
+                new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
+        final Date validityEndDate =
+                new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
+
+        // Generate a keypair
+        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(512);
+        final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+        final X500Principal dnName = new X500Principal("CN=test.android.com");
+        final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+        certGen.setSubjectDN(dnName);
+        certGen.setIssuerDN(dnName);
+        certGen.setNotBefore(validityBeginDate);
+        certGen.setNotAfter(validityEndDate);
+        certGen.setPublicKey(keyPair.getPublic());
+        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+        final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
+        return new CertificateAndKey(cert, keyPair.getPrivate());
+    }
+}
diff --git a/services/tests/VpnTests/java/android/net/VpnManagerTest.java b/services/tests/VpnTests/java/android/net/VpnManagerTest.java
new file mode 100644
index 0000000..f5b83f0
--- /dev/null
+++ b/services/tests/VpnTests/java/android/net/VpnManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.test.mock.MockContext;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.net.VpnProfile;
+import com.android.internal.util.MessageUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link VpnManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VpnManagerTest {
+
+    private static final String PKG_NAME = "fooPackage";
+
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
+
+    private IVpnManager mMockService;
+    private VpnManager mVpnManager;
+    private final MockContext mMockContext =
+            new MockContext() {
+                @Override
+                public String getOpPackageName() {
+                    return PKG_NAME;
+                }
+            };
+
+    @Before
+    public void setUp() throws Exception {
+        assumeFalse("Skipping test because watches don't support VPN",
+                InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_WATCH));
+        mMockService = mock(IVpnManager.class);
+        mVpnManager = new VpnManager(mMockContext, mMockService);
+    }
+
+    @Test
+    public void testProvisionVpnProfilePreconsented() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
+                .thenReturn(true);
+
+        // Expect there to be no intent returned, as consent has already been granted.
+        assertNull(mVpnManager.provisionVpnProfile(profile));
+        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testProvisionVpnProfileNeedsConsent() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
+                .thenReturn(false);
+
+        // Expect intent to be returned, as consent has not already been granted.
+        final Intent intent = mVpnManager.provisionVpnProfile(profile);
+        assertNotNull(intent);
+
+        final ComponentName expectedComponentName =
+                ComponentName.unflattenFromString(
+                        "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
+        assertEquals(expectedComponentName, intent.getComponent());
+        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testDeleteProvisionedVpnProfile() throws Exception {
+        mVpnManager.deleteProvisionedVpnProfile();
+        verify(mMockService).deleteVpnProfile(eq(PKG_NAME));
+    }
+
+    @Test
+    public void testStartProvisionedVpnProfile() throws Exception {
+        mVpnManager.startProvisionedVpnProfile();
+        verify(mMockService).startVpnProfile(eq(PKG_NAME));
+    }
+
+    @Test
+    public void testStopProvisionedVpnProfile() throws Exception {
+        mVpnManager.stopProvisionedVpnProfile();
+        verify(mMockService).stopVpnProfile(eq(PKG_NAME));
+    }
+
+    private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
+        return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                .setBypassable(true)
+                .setMaxMtu(1300)
+                .setMetered(true)
+                .setAuthPsk(PSK_BYTES)
+                .build();
+    }
+
+    @Test
+    public void testVpnTypesEqual() throws Exception {
+        SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames(
+                new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" });
+        SparseArray<String> nativeVpnType = MessageUtils.findMessageNames(
+                new Class[] { NativeVpnType.class }, new String[]{ "" });
+
+        // TYPE_VPN_NONE = -1 is only defined in VpnManager.
+        assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size());
+        for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) {
+            assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i));
+        }
+    }
+}
diff --git a/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
new file mode 100644
index 0000000..acbe8b8
--- /dev/null
+++ b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2019 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.internal.net;
+
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpSecAlgorithm;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Unit tests for {@link VpnProfile}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VpnProfileTest {
+    private static final String DUMMY_PROFILE_KEY = "Test";
+
+    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;
+    private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26;
+    private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27;
+    private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28;
+    private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29;
+
+    @Test
+    public void testDefaults() throws Exception {
+        final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
+
+        assertEquals(DUMMY_PROFILE_KEY, p.key);
+        assertEquals("", p.name);
+        assertEquals(VpnProfile.TYPE_PPTP, p.type);
+        assertEquals("", p.server);
+        assertEquals("", p.username);
+        assertEquals("", p.password);
+        assertEquals("", p.dnsServers);
+        assertEquals("", p.searchDomains);
+        assertEquals("", p.routes);
+        assertTrue(p.mppe);
+        assertEquals("", p.l2tpSecret);
+        assertEquals("", p.ipsecIdentifier);
+        assertEquals("", p.ipsecSecret);
+        assertEquals("", p.ipsecUserCert);
+        assertEquals("", p.ipsecCaCert);
+        assertEquals("", p.ipsecServerCert);
+        assertEquals(null, p.proxy);
+        assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
+        assertFalse(p.isBypassable);
+        assertFalse(p.isMetered);
+        assertEquals(1360, p.maxMtu);
+        assertFalse(p.areAuthParamsInline);
+        assertFalse(p.isRestrictedToTestNetworks);
+        assertFalse(p.excludeLocalRoutes);
+        assertFalse(p.requiresInternetValidation);
+        assertFalse(p.automaticNattKeepaliveTimerEnabled);
+        assertFalse(p.automaticIpVersionSelectionEnabled);
+    }
+
+    private VpnProfile getSampleIkev2Profile(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */,
+                true /* automaticIpVersionSelectionEnabled */);
+
+        p.name = "foo";
+        p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
+        p.server = "bar";
+        p.username = "baz";
+        p.password = "qux";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.l2tpSecret = "";
+        p.ipsecIdentifier = "quux";
+        p.ipsecSecret = "quuz";
+        p.ipsecUserCert = "corge";
+        p.ipsecCaCert = "grault";
+        p.ipsecServerCert = "garply";
+        p.proxy = null;
+        p.setAllowedAlgorithms(
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.CRYPT_AES_CBC));
+        p.isBypassable = true;
+        p.isMetered = true;
+        p.maxMtu = 1350;
+        p.areAuthParamsInline = true;
+
+        // Not saved, but also not compared.
+        p.saveLogin = true;
+
+        return p;
+    }
+
+    private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS),
+                true /* mAutomaticNattKeepaliveTimerEnabled */,
+                true /* automaticIpVersionSelectionEnabled */);
+
+        p.name = "foo";
+        p.server = "bar";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.proxy = null;
+        p.setAllowedAlgorithms(
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.CRYPT_AES_CBC));
+        p.isBypassable = true;
+        p.isMetered = true;
+        p.maxMtu = 1350;
+        p.areAuthParamsInline = true;
+
+        // Not saved, but also not compared.
+        p.saveLogin = true;
+
+        return p;
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(
+                getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
+
+        final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        modified.maxMtu--;
+        assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
+    }
+
+    @Test
+    public void testParcelUnparcel() {
+        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
+        assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
+    }
+
+    @Test
+    public void testEncodeDecodeWithIkeTunConnParams() {
+        final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testEncodeDecode() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testEncodeDecodeTooManyValues() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final byte[] tooManyValues =
+                (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
+
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
+    }
+
+    private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
+        // Sort to ensure when we remove, we can do it from greatest first.
+        Arrays.sort(missingIndices);
+
+        final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
+        final List<String> parts =
+                new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
+
+        // Remove from back first to ensure indexing is consistent.
+        for (int i = missingIndices.length - 1; i >= 0; i--) {
+            parts.remove(missingIndices[i]);
+        }
+
+        return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
+    }
+
+    @Test
+    public void testEncodeDecodeInvalidNumberOfValues() {
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_AUTH_PARAMS_INLINE,
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
+                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
+                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
+                        ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
+                        ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
+                        ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED
+                        /* missingIndices */);
+
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
+    }
+
+    private String getEncodedDecodedIkev2ProfileWithtooFewValues() {
+        return getEncodedDecodedIkev2ProfileMissingValues(
+                ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
+                ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
+                ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
+                ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
+                ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
+                ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without isRestrictedToTestNetworks defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.isRestrictedToTestNetworks);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingExcludeLocalRoutes() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without excludeLocalRoutes defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.excludeLocalRoutes);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingRequiresValidation() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without requiresValidation defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.requiresInternetValidation);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.automaticNattKeepaliveTimerEnabled);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without automaticIpVersionSelectionEnabled defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.automaticIpVersionSelectionEnabled);
+    }
+
+    @Test
+    public void testEncodeDecodeLoginsNotSaved() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        profile.saveLogin = false;
+
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertNotEquals(profile, decoded);
+
+        // Add the username/password back, everything else must be equal.
+        decoded.username = profile.username;
+        decoded.password = profile.password;
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testClone() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final VpnProfile clone = profile.clone();
+        assertEquals(profile, clone);
+        assertNotSame(profile, clone);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/LockdownVpnTrackerTest.java b/services/tests/VpnTests/java/com/android/server/net/LockdownVpnTrackerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/net/LockdownVpnTrackerTest.java
rename to services/tests/VpnTests/java/com/android/server/net/LockdownVpnTrackerTest.java
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index e914726..28a5864 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -1802,6 +1802,59 @@
     }
 
     @Test
+    public void testReplacePendingToCachedProcess_withDeferrableBroadcast() throws Exception {
+        // Legacy stack doesn't support deferral
+        Assume.assumeTrue(mImpl == Impl.MODERN);
+
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
+
+        setProcessFreezable(receiverGreenApp, true, false);
+        mQueue.onProcessFreezableChangedLocked(receiverGreenApp);
+        waitForIdle();
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK)
+                .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        final BroadcastOptions opts = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE);
+
+        final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 10);
+        final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 5);
+        final BroadcastFilter receiverYellow = makeRegisteredReceiver(receiverYellowApp, 0);
+        enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, opts, List.of(
+                receiverGreen, receiverBlue, receiverYellow)));
+
+        // Enqueue the broadcast again to replace the earlier one
+        enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, opts, List.of(
+                receiverGreen, receiverBlue, receiverYellow)));
+
+        waitForIdle();
+        // Green should still be in the cached state and shouldn't receive the broadcast
+        verifyScheduleRegisteredReceiver(never(), receiverGreenApp, timeTick);
+
+        final IApplicationThread blueThread = receiverBlueApp.getThread();
+        final IApplicationThread yellowThread = receiverYellowApp.getThread();
+        final InOrder inOrder = inOrder(blueThread, yellowThread);
+        inOrder.verify(blueThread).scheduleRegisteredReceiver(
+                any(), argThat(filterEqualsIgnoringComponent(timeTick)),
+                anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(),
+                eq(UserHandle.USER_SYSTEM), anyInt(), anyInt(), any());
+        inOrder.verify(yellowThread).scheduleRegisteredReceiver(
+                any(), argThat(filterEqualsIgnoringComponent(timeTick)),
+                anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(),
+                eq(UserHandle.USER_SYSTEM), anyInt(), anyInt(), any());
+
+        setProcessFreezable(receiverGreenApp, false, false);
+        mQueue.onProcessFreezableChangedLocked(receiverGreenApp);
+        waitForIdle();
+
+        // Confirm that green receives the broadcast once it comes out of the cached state
+        verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, timeTick);
+    }
+
+    @Test
     public void testIdleAndBarrier() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index bb00634..fa1fd90 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -344,6 +344,21 @@
         verifyNoMoreInteractions(mSensorManager);
     }
 
+    @Test
+    public void testAwaitLuxWhenNoLightSensor() {
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(null);
+        mProbe = new ALSProbe(mSensorManager, new Handler(mLooper.getLooper()), TIMEOUT_MS - 1);
+
+        AtomicInteger lux = new AtomicInteger(-5);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        // Verify that no light sensor will be registered.
+        verify(mSensorManager, times(0)).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        assertThat(lux.get()).isEqualTo(-1);
+    }
+
     private void moveTimeBy(long millis) {
         mLooper.moveTimeForward(millis);
         mLooper.processAllMessages();
diff --git a/tests/graphics/HwAccelerationTest/jni/native-lib.cpp b/tests/graphics/HwAccelerationTest/jni/native-lib.cpp
index 407d4bf..2977c21 100644
--- a/tests/graphics/HwAccelerationTest/jni/native-lib.cpp
+++ b/tests/graphics/HwAccelerationTest/jni/native-lib.cpp
@@ -30,7 +30,7 @@
 
     void setBuffer(AHardwareBuffer* buffer) {
         ASurfaceTransaction* transaction = ASurfaceTransaction_create();
-        ASurfaceTransaction_setBuffer(transaction, surfaceControl, buffer);
+        ASurfaceTransaction_setBuffer(transaction, surfaceControl, buffer, -1);
         ASurfaceTransaction_setVisibility(transaction, surfaceControl,
                                           ASURFACE_TRANSACTION_VISIBILITY_SHOW);
         ASurfaceTransaction_apply(transaction);
diff --git a/tools/streaming_proto/Android.bp b/tools/streaming_proto/Android.bp
index b18bdff..b1b314fc 100644
--- a/tools/streaming_proto/Android.bp
+++ b/tools/streaming_proto/Android.bp
@@ -17,6 +17,7 @@
 // ==========================================================
 // Build the host executable: protoc-gen-javastream
 // ==========================================================
+
 package {
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
@@ -41,6 +42,32 @@
     static_libs: ["libprotoc"],
 }
 
+// ==========================================================
+// Build the host static library: java_streaming_proto_lib
+// ==========================================================
+
+cc_library_host_static {
+    name: "java_streaming_proto_lib",
+    defaults: ["protoc-gen-stream-defaults"],
+    target: {
+        darwin: {
+            cflags: ["-D_DARWIN_UNLIMITED_STREAMS"],
+        },
+    },
+    cflags: [
+        "-Wno-format-y2k",
+        "-DSTATIC_ANDROIDFW_FOR_TOOLS",
+    ],
+
+    srcs: [
+        "java/java_proto_stream_code_generator.cpp",
+    ],
+}
+
+// ==========================================================
+// Build the host executable: protoc-gen-javastream
+// ==========================================================
+
 cc_binary_host {
     name: "protoc-gen-javastream",
     srcs: [
@@ -48,8 +75,13 @@
     ],
 
     defaults: ["protoc-gen-stream-defaults"],
+    static_libs: ["java_streaming_proto_lib"],
 }
 
+// ==========================================================
+// Build the host executable: protoc-gen-cppstream
+// ==========================================================
+
 cc_binary_host {
     name: "protoc-gen-cppstream",
     srcs: [
@@ -60,13 +92,31 @@
 }
 
 // ==========================================================
+// Build the host tests: StreamingProtoTest
+// ==========================================================
+
+cc_test_host {
+    name: "StreamingProtoTest",
+    defaults: ["protoc-gen-stream-defaults"],
+    srcs: [
+        "test/unit/**/*.cpp",
+    ],
+    static_libs: [
+        "java_streaming_proto_lib",
+        "libgmock",
+        "libgtest",
+    ],
+}
+
+// ==========================================================
 // Build the java test
 // ==========================================================
+
 java_library {
-    name: "StreamingProtoTest",
+    name: "StreamingProtoJavaIntegrationTest",
     srcs: [
-        "test/**/*.java",
-        "test/**/*.proto",
+        "test/integration/**/*.java",
+        "test/integration/**/*.proto",
     ],
     proto: {
         type: "stream",
diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.cpp b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp
new file mode 100644
index 0000000..9d61111
--- /dev/null
+++ b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#include "java_proto_stream_code_generator.h"
+
+#include <stdio.h>
+
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <sstream>
+#include <string>
+
+#include "Errors.h"
+
+using namespace android::stream_proto;
+using namespace google::protobuf::io;
+using namespace std;
+
+/**
+ * If the descriptor gives us a class name, use that. Otherwise make one up from
+ * the filename of the .proto file.
+ */
+static string make_outer_class_name(const FileDescriptorProto& file_descriptor) {
+    string name = file_descriptor.options().java_outer_classname();
+    if (name.size() == 0) {
+        name = to_camel_case(file_base_name(file_descriptor.name()));
+        if (name.size() == 0) {
+            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE,
+                       "Unable to make an outer class name for file: %s",
+                       file_descriptor.name().c_str());
+            name = "Unknown";
+        }
+    }
+    return name;
+}
+
+/**
+ * Figure out the package name that we are generating.
+ */
+static string make_java_package(const FileDescriptorProto& file_descriptor) {
+    if (file_descriptor.options().has_java_package()) {
+        return file_descriptor.options().java_package();
+    } else {
+        return file_descriptor.package();
+    }
+}
+
+/**
+ * Figure out the name of the file we are generating.
+ */
+static string make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name) {
+    string const package = make_java_package(file_descriptor);
+    string result;
+    if (package.size() > 0) {
+        result = replace_string(package, '.', '/');
+        result += '/';
+    }
+
+    result += class_name;
+    result += ".java";
+
+    return result;
+}
+
+static string indent_more(const string& indent) {
+    return indent + INDENT;
+}
+
+/**
+ * Write the constants for an enum.
+ */
+static void write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent) {
+    const int N = enu.value_size();
+    text << indent << "// enum " << enu.name() << endl;
+    for (int i = 0; i < N; i++) {
+        const EnumValueDescriptorProto& value = enu.value(i);
+        text << indent << "public static final int " << make_constant_name(value.name()) << " = "
+             << value.number() << ";" << endl;
+    }
+    text << endl;
+}
+
+/**
+ * Write a field.
+ */
+static void write_field(stringstream& text, const FieldDescriptorProto& field,
+                        const string& indent) {
+    string optional_comment =
+            field.label() == FieldDescriptorProto::LABEL_OPTIONAL ? "optional " : "";
+    string repeated_comment =
+            field.label() == FieldDescriptorProto::LABEL_REPEATED ? "repeated " : "";
+    string proto_type = get_proto_type(field);
+    string packed_comment = field.options().packed() ? " [packed=true]" : "";
+    text << indent << "// " << optional_comment << repeated_comment << proto_type << ' '
+         << field.name() << " = " << field.number() << packed_comment << ';' << endl;
+
+    text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x";
+
+    ios::fmtflags fmt(text.flags());
+    text << setfill('0') << setw(16) << hex << get_field_id(field);
+    text.flags(fmt);
+
+    text << "L;" << endl;
+
+    text << endl;
+}
+
+/**
+ * Write a Message constants class.
+ */
+static void write_message(stringstream& text, const DescriptorProto& message,
+                          const string& indent) {
+    int N;
+    const string indented = indent_more(indent);
+
+    text << indent << "// message " << message.name() << endl;
+    text << indent << "public final class " << message.name() << " {" << endl;
+    text << endl;
+
+    // Enums
+    N = message.enum_type_size();
+    for (int i = 0; i < N; i++) {
+        write_enum(text, message.enum_type(i), indented);
+    }
+
+    // Nested classes
+    N = message.nested_type_size();
+    for (int i = 0; i < N; i++) {
+        write_message(text, message.nested_type(i), indented);
+    }
+
+    // Fields
+    N = message.field_size();
+    for (int i = 0; i < N; i++) {
+        write_field(text, message.field(i), indented);
+    }
+
+    text << indent << "}" << endl;
+    text << endl;
+}
+
+/**
+ * Write the contents of a file.
+ *
+ * If there are enums and generate_outer is false, invalid java code will be generated.
+ */
+static void write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor,
+                       const string& filename, bool generate_outer,
+                       const vector<EnumDescriptorProto>& enums,
+                       const vector<DescriptorProto>& messages) {
+    stringstream text;
+
+    string const package_name = make_java_package(file_descriptor);
+    string const outer_class_name = make_outer_class_name(file_descriptor);
+
+    text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl;
+    text << "// source: " << file_descriptor.name() << endl << endl;
+
+    if (package_name.size() > 0) {
+        if (package_name.size() > 0) {
+            text << "package " << package_name << ";" << endl;
+            text << endl;
+        }
+    }
+
+    // This bit of policy is android api rules specific: Raw proto classes
+    // must never be in the API
+    text << "/** @hide */" << endl;
+    //    text << "@android.annotation.TestApi" << endl;
+
+    if (generate_outer) {
+        text << "public final class " << outer_class_name << " {" << endl;
+        text << endl;
+    }
+
+    size_t N;
+    const string indented = generate_outer ? indent_more("") : string();
+
+    N = enums.size();
+    for (size_t i = 0; i < N; i++) {
+        write_enum(text, enums[i], indented);
+    }
+
+    N = messages.size();
+    for (size_t i = 0; i < N; i++) {
+        write_message(text, messages[i], indented);
+    }
+
+    if (generate_outer) {
+        text << "}" << endl;
+    }
+
+    CodeGeneratorResponse::File* file_response = response->add_file();
+    file_response->set_name(filename);
+    file_response->set_content(text.str());
+}
+
+/**
+ * Write one file per class.  Put all of the enums into the "outer" class.
+ */
+static void write_multiple_files(CodeGeneratorResponse* response,
+                                 const FileDescriptorProto& file_descriptor,
+                                 set<string> messages_to_compile) {
+    // If there is anything to put in the outer class file, create one
+    if (file_descriptor.enum_type_size() > 0) {
+        vector<EnumDescriptorProto> enums;
+        int N = file_descriptor.enum_type_size();
+        for (int i = 0; i < N; i++) {
+            auto enum_full_name =
+                    file_descriptor.package() + "." + file_descriptor.enum_type(i).name();
+            if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) {
+                continue;
+            }
+            enums.push_back(file_descriptor.enum_type(i));
+        }
+
+        vector<DescriptorProto> messages;
+
+        if (messages_to_compile.empty() || !enums.empty()) {
+            write_file(response, file_descriptor,
+                       make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
+                       true, enums, messages);
+        }
+    }
+
+    // For each of the message types, make a file
+    int N = file_descriptor.message_type_size();
+    for (int i = 0; i < N; i++) {
+        vector<EnumDescriptorProto> enums;
+
+        vector<DescriptorProto> messages;
+
+        auto message_full_name =
+                file_descriptor.package() + "." + file_descriptor.message_type(i).name();
+        if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) {
+            continue;
+        }
+        messages.push_back(file_descriptor.message_type(i));
+
+        if (messages_to_compile.empty() || !messages.empty()) {
+            write_file(response, file_descriptor,
+                       make_file_name(file_descriptor, file_descriptor.message_type(i).name()),
+                       false, enums, messages);
+        }
+    }
+}
+
+static void write_single_file(CodeGeneratorResponse* response,
+                              const FileDescriptorProto& file_descriptor,
+                              set<string> messages_to_compile) {
+    int N;
+
+    vector<EnumDescriptorProto> enums;
+    N = file_descriptor.enum_type_size();
+    for (int i = 0; i < N; i++) {
+        auto enum_full_name = file_descriptor.package() + "." + file_descriptor.enum_type(i).name();
+        if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) {
+            continue;
+        }
+
+        enums.push_back(file_descriptor.enum_type(i));
+    }
+
+    vector<DescriptorProto> messages;
+    N = file_descriptor.message_type_size();
+    for (int i = 0; i < N; i++) {
+        auto message_full_name =
+                file_descriptor.package() + "." + file_descriptor.message_type(i).name();
+
+        if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) {
+            continue;
+        }
+
+        messages.push_back(file_descriptor.message_type(i));
+    }
+
+    if (messages_to_compile.empty() || !enums.empty() || !messages.empty()) {
+        write_file(response, file_descriptor,
+                   make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), true,
+                   enums, messages);
+    }
+}
+
+static void parse_args_string(stringstream args_string_stream,
+                              set<string>* messages_to_compile_out) {
+    string line;
+    while (getline(args_string_stream, line, ';')) {
+        stringstream line_ss(line);
+        string arg_name;
+        getline(line_ss, arg_name, ':');
+        if (arg_name == "include_filter") {
+            string full_message_name;
+            while (getline(line_ss, full_message_name, ',')) {
+                messages_to_compile_out->insert(full_message_name);
+            }
+        } else {
+            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE, "Unexpected argument '%s'.", arg_name.c_str());
+        }
+    }
+}
+
+CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request) {
+    CodeGeneratorResponse response;
+
+    set<string> messages_to_compile;
+    auto request_params = request.parameter();
+    if (!request_params.empty()) {
+        parse_args_string(stringstream(request_params), &messages_to_compile);
+    }
+
+    // Build the files we need.
+    const int N = request.proto_file_size();
+    for (int i = 0; i < N; i++) {
+        const FileDescriptorProto& file_descriptor = request.proto_file(i);
+        if (should_generate_for_file(request, file_descriptor.name())) {
+            if (file_descriptor.options().java_multiple_files()) {
+                write_multiple_files(&response, file_descriptor, messages_to_compile);
+            } else {
+                write_single_file(&response, file_descriptor, messages_to_compile);
+            }
+        }
+    }
+
+    return response;
+}
diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.h b/tools/streaming_proto/java/java_proto_stream_code_generator.h
new file mode 100644
index 0000000..d2492f7
--- /dev/null
+++ b/tools/streaming_proto/java/java_proto_stream_code_generator.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#ifndef AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
+#define AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
+
+#include "stream_proto_utils.h"
+#include "string_utils.h"
+
+using namespace android::stream_proto;
+using namespace google::protobuf::io;
+using namespace std;
+
+CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request);
+
+#endif // AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
\ No newline at end of file
diff --git a/tools/streaming_proto/java/main.cpp b/tools/streaming_proto/java/main.cpp
index c9c50a5..5b35504 100644
--- a/tools/streaming_proto/java/main.cpp
+++ b/tools/streaming_proto/java/main.cpp
@@ -1,268 +1,21 @@
-#include "Errors.h"
-#include "stream_proto_utils.h"
-#include "string_utils.h"
-
 #include <stdio.h>
+
 #include <iomanip>
 #include <iostream>
-#include <sstream>
 #include <map>
+#include <sstream>
+#include <string>
+
+#include "Errors.h"
+#include "java_proto_stream_code_generator.h"
+#include "stream_proto_utils.h"
 
 using namespace android::stream_proto;
 using namespace google::protobuf::io;
 using namespace std;
 
 /**
- * If the descriptor gives us a class name, use that. Otherwise make one up from
- * the filename of the .proto file.
- */
-static string
-make_outer_class_name(const FileDescriptorProto& file_descriptor)
-{
-    string name = file_descriptor.options().java_outer_classname();
-    if (name.size() == 0) {
-        name = to_camel_case(file_base_name(file_descriptor.name()));
-        if (name.size() == 0) {
-            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE,
-                    "Unable to make an outer class name for file: %s",
-                    file_descriptor.name().c_str());
-            name = "Unknown";
-        }
-    }
-    return name;
-}
-
-/**
- * Figure out the package name that we are generating.
- */
-static string
-make_java_package(const FileDescriptorProto& file_descriptor) {
-    if (file_descriptor.options().has_java_package()) {
-        return file_descriptor.options().java_package();
-    } else {
-        return file_descriptor.package();
-    }
-}
-
-/**
- * Figure out the name of the file we are generating.
- */
-static string
-make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name)
-{
-    string const package = make_java_package(file_descriptor);
-    string result;
-    if (package.size() > 0) {
-        result = replace_string(package, '.', '/');
-        result += '/';
-    }
-
-    result += class_name;
-    result += ".java";
-
-    return result;
-}
-
-static string
-indent_more(const string& indent)
-{
-    return indent + INDENT;
-}
-
-/**
- * Write the constants for an enum.
- */
-static void
-write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent)
-{
-    const int N = enu.value_size();
-    text << indent << "// enum " << enu.name() << endl;
-    for (int i=0; i<N; i++) {
-        const EnumValueDescriptorProto& value = enu.value(i);
-        text << indent << "public static final int "
-                << make_constant_name(value.name())
-                << " = " << value.number() << ";" << endl;
-    }
-    text << endl;
-}
-
-/**
- * Write a field.
- */
-static void
-write_field(stringstream& text, const FieldDescriptorProto& field, const string& indent)
-{
-    string optional_comment = field.label() == FieldDescriptorProto::LABEL_OPTIONAL
-            ? "optional " : "";
-    string repeated_comment = field.label() == FieldDescriptorProto::LABEL_REPEATED
-            ? "repeated " : "";
-    string proto_type = get_proto_type(field);
-    string packed_comment = field.options().packed()
-            ? " [packed=true]" : "";
-    text << indent << "// " << optional_comment << repeated_comment << proto_type << ' '
-            << field.name() << " = " << field.number() << packed_comment << ';' << endl;
-
-    text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x";
-
-    ios::fmtflags fmt(text.flags());
-    text << setfill('0') << setw(16) << hex << get_field_id(field);
-    text.flags(fmt);
-
-    text << "L;" << endl;
-
-    text << endl;
-}
-
-/**
- * Write a Message constants class.
- */
-static void
-write_message(stringstream& text, const DescriptorProto& message, const string& indent)
-{
-    int N;
-    const string indented = indent_more(indent);
-
-    text << indent << "// message " << message.name() << endl;
-    text << indent << "public final class " << message.name() << " {" << endl;
-    text << endl;
-
-    // Enums
-    N = message.enum_type_size();
-    for (int i=0; i<N; i++) {
-        write_enum(text, message.enum_type(i), indented);
-    }
-
-    // Nested classes
-    N = message.nested_type_size();
-    for (int i=0; i<N; i++) {
-        write_message(text, message.nested_type(i), indented);
-    }
-
-    // Fields
-    N = message.field_size();
-    for (int i=0; i<N; i++) {
-        write_field(text, message.field(i), indented);
-    }
-
-    text << indent << "}" << endl;
-    text << endl;
-}
-
-/**
- * Write the contents of a file.
  *
- * If there are enums and generate_outer is false, invalid java code will be generated.
- */
-static void
-write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor,
-        const string& filename, bool generate_outer,
-        const vector<EnumDescriptorProto>& enums, const vector<DescriptorProto>& messages)
-{
-    stringstream text;
-
-    string const package_name = make_java_package(file_descriptor);
-    string const outer_class_name = make_outer_class_name(file_descriptor);
-
-    text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl;
-    text << "// source: " << file_descriptor.name() << endl << endl;
-
-    if (package_name.size() > 0) {
-        if (package_name.size() > 0) {
-            text << "package " << package_name << ";" << endl;
-            text << endl;
-        }
-    }
-
-    // This bit of policy is android api rules specific: Raw proto classes
-    // must never be in the API
-    text << "/** @hide */" << endl;
-//    text << "@android.annotation.TestApi" << endl;
-
-    if (generate_outer) {
-        text << "public final class " << outer_class_name << " {" << endl;
-        text << endl;
-    }
-
-    size_t N;
-    const string indented = generate_outer ? indent_more("") : string();
-    
-    N = enums.size();
-    for (size_t i=0; i<N; i++) {
-        write_enum(text, enums[i], indented);
-    }
-
-    N = messages.size();
-    for (size_t i=0; i<N; i++) {
-        write_message(text, messages[i], indented);
-    }
-
-    if (generate_outer) {
-        text << "}" << endl;
-    }
-
-    CodeGeneratorResponse::File* file_response = response->add_file();
-    file_response->set_name(filename);
-    file_response->set_content(text.str());
-}
-
-/**
- * Write one file per class.  Put all of the enums into the "outer" class.
- */
-static void
-write_multiple_files(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor)
-{
-    // If there is anything to put in the outer class file, create one
-    if (file_descriptor.enum_type_size() > 0) {
-        vector<EnumDescriptorProto> enums;
-        int N = file_descriptor.enum_type_size();
-        for (int i=0; i<N; i++) {
-            enums.push_back(file_descriptor.enum_type(i));
-        }
-
-        vector<DescriptorProto> messages;
-
-        write_file(response, file_descriptor,
-                make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
-                true, enums, messages);
-    }
-
-    // For each of the message types, make a file
-    int N = file_descriptor.message_type_size();
-    for (int i=0; i<N; i++) {
-        vector<EnumDescriptorProto> enums;
-
-        vector<DescriptorProto> messages;
-        messages.push_back(file_descriptor.message_type(i));
-
-        write_file(response, file_descriptor,
-                make_file_name(file_descriptor, file_descriptor.message_type(i).name()),
-                false, enums, messages);
-    }
-}
-
-static void
-write_single_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor)
-{
-    int N;
-
-    vector<EnumDescriptorProto> enums;
-    N = file_descriptor.enum_type_size();
-    for (int i=0; i<N; i++) {
-        enums.push_back(file_descriptor.enum_type(i));
-    }
-
-    vector<DescriptorProto> messages;
-    N = file_descriptor.message_type_size();
-    for (int i=0; i<N; i++) {
-        messages.push_back(file_descriptor.message_type(i));
-    }
-
-    write_file(response, file_descriptor,
-            make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
-            true, enums, messages);
-}
-
-/**
  * Main.
  */
 int
@@ -273,24 +26,11 @@
 
     GOOGLE_PROTOBUF_VERIFY_VERSION;
 
-    CodeGeneratorRequest request;
-    CodeGeneratorResponse response;
-
     // Read the request
+    CodeGeneratorRequest request;
     request.ParseFromIstream(&cin);
 
-    // Build the files we need.
-    const int N = request.proto_file_size();
-    for (int i=0; i<N; i++) {
-        const FileDescriptorProto& file_descriptor = request.proto_file(i);
-        if (should_generate_for_file(request, file_descriptor.name())) {
-            if (file_descriptor.options().java_multiple_files()) {
-                write_multiple_files(&response, file_descriptor);
-            } else {
-                write_single_file(&response, file_descriptor);
-            }
-        }
-    }
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
 
     // If we had errors, don't write the response. Print the errors and exit.
     if (ERRORS.HasErrors()) {
diff --git a/tools/streaming_proto/test/imported.proto b/tools/streaming_proto/test/integration/imported.proto
similarity index 100%
rename from tools/streaming_proto/test/imported.proto
rename to tools/streaming_proto/test/integration/imported.proto
diff --git a/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
new file mode 100644
index 0000000..2a7001b
--- /dev/null
+++ b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.streaming_proto_test;
+
+public class Main {
+    public void main(String[] argv) {
+        System.out.println("hello world");
+    }
+}
diff --git a/tools/streaming_proto/test/test.proto b/tools/streaming_proto/test/integration/test.proto
similarity index 97%
rename from tools/streaming_proto/test/test.proto
rename to tools/streaming_proto/test/integration/test.proto
index de80ed6..3cf81b4 100644
--- a/tools/streaming_proto/test/test.proto
+++ b/tools/streaming_proto/test/integration/test.proto
@@ -16,7 +16,7 @@
 
 syntax = "proto2";
 
-import "frameworks/base/tools/streaming_proto/test/imported.proto";
+import "frameworks/base/tools/streaming_proto/test/integration/imported.proto";
 
 package com.android.streaming_proto_test;
 
diff --git a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java b/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java
deleted file mode 100644
index 1246f53..0000000
--- a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.android.streaming_proto_test;
-
-public class Main {
-    public void main(String[] argv) {
-        System.out.println("hello world");
-    }
-}
diff --git a/tools/streaming_proto/test/unit/streaming_proto_java.cpp b/tools/streaming_proto/test/unit/streaming_proto_java.cpp
new file mode 100644
index 0000000..8df9716
--- /dev/null
+++ b/tools/streaming_proto/test/unit/streaming_proto_java.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "java/java_proto_stream_code_generator.h"
+
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+static void add_my_test_proto_file(CodeGeneratorRequest* request) {
+    request->add_file_to_generate("MyTestProtoFile");
+
+    FileDescriptorProto* file_desc = request->add_proto_file();
+    file_desc->set_name("MyTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(false);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_message");
+}
+
+static void add_my_other_test_proto_file(CodeGeneratorRequest* request) {
+    request->add_file_to_generate("MyOtherTestProtoFile");
+
+    FileDescriptorProto* file_desc = request->add_proto_file();
+    file_desc->set_name("MyOtherTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(false);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyOtherTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("a_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("another_test_field");
+}
+
+static CodeGeneratorRequest create_simple_two_file_request() {
+    CodeGeneratorRequest request;
+
+    add_my_test_proto_file(&request);
+    add_my_other_test_proto_file(&request);
+
+    return request;
+}
+
+static CodeGeneratorRequest create_simple_multi_file_request() {
+    CodeGeneratorRequest request;
+
+    request.add_file_to_generate("MyMultiMessageTestProtoFile");
+
+    FileDescriptorProto* file_desc = request.add_proto_file();
+    file_desc->set_name("MyMultiMessageTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(true);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_message");
+
+    message = file_desc->add_message_type();
+    message->set_name("MyOtherTestMessage");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("a_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("another_test_field");
+
+    return request;
+}
+
+TEST(StreamingProtoJavaTest, NoFilter) {
+    CodeGeneratorRequest request = create_simple_two_file_request();
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 2);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java");
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+
+    EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestProtoFile.java");
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestProtoFile"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithFilter) {
+    CodeGeneratorRequest request = create_simple_two_file_request();
+    request.set_parameter("include_filter:test.package.MyTestMessage");
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 1);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java");
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithoutFilter_MultipleJavaFiles) {
+    CodeGeneratorRequest request = create_simple_multi_file_request();
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 2);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java");
+    EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile")));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+
+    EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestMessage.java");
+    EXPECT_THAT(response.file(1).content(), Not(HasSubstr("class MyOtherTestProtoFile")));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithFilter_MultipleJavaFiles) {
+    CodeGeneratorRequest request = create_simple_multi_file_request();
+    request.set_parameter("include_filter:test.package.MyTestMessage");
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 1);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java");
+    EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile")));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+}