Support measuring split

measurePackage is changed into measureApk, which would hint that the
method deals with a file instead of a "package" in the package manager's
term.

collectAppInfo is added to measure an app package, which may include
split.

Test
 * Add a test that leverages debug.transparency.bg-install-apps of BICs.
 * Copy BaseInstallMultiple from another test in frameworks/base to
   support split install.

Misc
 * Delete getApexInfo and corresponding unit tests, since it's now
   coverred through collectAllApexInfo in integration test. This also
   makes it easy for refactoring without having to keeping the Bundle
   works as the return type.

Bug: 264296226
Test: atest BinaryTransparencyServiceTest BinaryTransparencyHostTest
Change-Id: Iaa4118dfa8605acda313dbcc2466ae96a60b4721
diff --git a/core/java/android/transparency/BinaryTransparencyManager.java b/core/java/android/transparency/BinaryTransparencyManager.java
index d77bbcc..c18adfc 100644
--- a/core/java/android/transparency/BinaryTransparencyManager.java
+++ b/core/java/android/transparency/BinaryTransparencyManager.java
@@ -67,24 +67,6 @@
     }
 
     /**
-     * Gets binary measurements of all installed APEXs, each packed in a Bundle.
-     * @return A List of {@link android.os.Bundle}s with the following keys:
-     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_PACKAGE_INFO}
-     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST_ALGORITHM}
-     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST}
-     */
-    // TODO(b/259422958): Fix static constants referenced here - should be defined here
-    @NonNull
-    public List getApexInfo() {
-        try {
-            Slog.d(TAG, "Calling backend's getApexInfo()");
-            return mService.getApexInfo();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Collects the APEX information on the device.
      *
      * @param includeTestOnly Whether to include test only data in the returned ApexInfo.
@@ -116,4 +98,21 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Collects the silent installed MBA information on the device.
+     *
+     * @return A List containing the MBA info of silent installed.
+     * @hide
+     */
+    @NonNull
+    public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo(
+            Bundle packagesToSkip) {
+        try {
+            Slog.d(TAG, "Calling backend's collectAllSilentInstalledMbaInfo()");
+            return mService.collectAllSilentInstalledMbaInfo(packagesToSkip);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
index e782aa7..c8340ac 100644
--- a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
+++ b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
@@ -28,8 +28,6 @@
 interface IBinaryTransparencyService {
     String getSignedImageInfo();
 
-    List getApexInfo();
-
     void recordMeasurementsForAllPackages();
 
     parcelable ApexInfo {
@@ -60,4 +58,5 @@
     /** Test only */
     List<ApexInfo> collectAllApexInfo(boolean includeTestOnly);
     List<AppInfo> collectAllUpdatedPreloadInfo(in Bundle packagesToSkip);
+    List<AppInfo> collectAllSilentInstalledMbaInfo(in Bundle packagesToSkip);
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index b6a2a0e..fc81675 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -34,6 +34,7 @@
 import android.content.IntentFilter;
 import android.content.pm.ApexStagedEvent;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.Checksum;
 import android.content.pm.IBackgroundInstallControlService;
 import android.content.pm.IPackageManagerNative;
 import android.content.pm.IStagedApexObserver;
@@ -84,6 +85,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.pm.ApexManager;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.AndroidPackageSplit;
 import com.android.server.pm.pkg.PackageState;
 
 import libcore.util.HexEncoding;
@@ -120,15 +122,6 @@
 
     static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000;
 
-    @VisibleForTesting
-    static final String BUNDLE_PACKAGE_NAME = "package-name";
-    @VisibleForTesting
-    static final String BUNDLE_PACKAGE_IS_APEX = "package-is-apex";
-    @VisibleForTesting
-    static final String BUNDLE_CONTENT_DIGEST_ALGORITHM = "content-digest-algo";
-    @VisibleForTesting
-    static final String BUNDLE_CONTENT_DIGEST = "content-digest";
-
     static final String APEX_PRELOAD_LOCATION_ERROR = "could-not-be-determined";
 
     // used for indicating any type of error during MBA measurement
@@ -170,29 +163,6 @@
             return mVbmetaDigest;
         }
 
-        @Override
-        public List getApexInfo() {
-            List<Bundle> results = new ArrayList<>();
-
-            for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
-                PackageState packageState = mPackageManagerInternal.getPackageStateInternal(
-                        packageInfo.packageName);
-                if (packageState == null) {
-                    Slog.w(TAG, "Package state is unavailable, ignoring the package "
-                            + packageInfo.packageName);
-                    continue;
-                }
-                Bundle apexMeasurement = measurePackage(packageState);
-                if (apexMeasurement == null) {
-                    Slog.w(TAG, "Skipping the missing APEX in " + packageState.getPath());
-                    continue;
-                }
-                results.add(apexMeasurement);
-            }
-
-            return results;
-        }
-
         /**
          * A helper function to compute the SHA256 digest of APK package signer.
          * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}.
@@ -217,58 +187,102 @@
             return resultList.toArray(new String[1]);
         }
 
-        /**
-         * Perform basic measurement (i.e. content digest) on a given package.
+        /*
+         * Perform basic measurement (i.e. content digest) on a given app, including the split APKs.
+         *
          * @param packageState The package to be measured.
-         * @return a {@link android.os.Bundle} that packs the measurement result with the following
-         *         keys: {@link #BUNDLE_PACKAGE_NAME},
-         *               {@link #BUNDLE_PACKAGE_IS_APEX}
-         *               {@link #BUNDLE_CONTENT_DIGEST_ALGORITHM}
-         *               {@link #BUNDLE_CONTENT_DIGEST}
+         * @param mbaStatus Assign this value of MBA status to the returned elements.
+         * @return a @{@code List<IBinaryTransparencyService.AppInfo>}
          */
-        private @Nullable Bundle measurePackage(PackageState packageState) {
-            Bundle result = new Bundle();
-
+        private @NonNull List<IBinaryTransparencyService.AppInfo> collectAppInfo(
+                PackageState packageState, int mbaStatus) {
             // compute content digest
             if (DEBUG) {
                 Slog.d(TAG, "Computing content digest for " + packageState.getPackageName() + " at "
                         + packageState.getPath());
             }
+
+            var results = new ArrayList<IBinaryTransparencyService.AppInfo>();
+
+            // Same attributes across base and splits.
+            String packageName = packageState.getPackageName();
+            long versionCode = packageState.getVersionCode();
+            String[] signerDigests =
+                    computePackageSignerSha256Digests(packageState.getSigningInfo());
+
             AndroidPackage pkg = packageState.getAndroidPackage();
-            if (pkg == null) {
-                Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
-                return null;
+            for (AndroidPackageSplit split : pkg.getSplits()) {
+                var appInfo = new IBinaryTransparencyService.AppInfo();
+                appInfo.packageName = packageName;
+                appInfo.longVersion = versionCode;
+                appInfo.splitName = split.getName();  // base's split name is null
+                // Signer digests are consistent between splits, guaranteed by Package Manager.
+                appInfo.signerDigests = signerDigests;
+                appInfo.mbaStatus = mbaStatus;
+
+                // Only digest and split name are different between splits.
+                Checksum checksum = measureApk(split.getPath());
+                appInfo.digest = checksum.getValue();
+                appInfo.digestAlgorithm = checksum.getType();
+
+                results.add(appInfo);
             }
-            Map<Integer, byte[]> contentDigests = computeApkContentDigest(pkg.getBaseApkPath());
-            result.putString(BUNDLE_PACKAGE_NAME, pkg.getPackageName());
+
+            // InstallSourceInfo is only available per package name, so store it only on the base
+            // APK. It's not current currently available in PackageState (there's a TODO), to we
+            // need to extract manually with another call.
+            //
+            // Base APK is already the 0-th split from getSplits() and can't be null.
+            AppInfo base = results.get(0);
+            InstallSourceInfo installSourceInfo = getInstallSourceInfo(
+                    packageState.getPackageName());
+            if (installSourceInfo != null) {
+                base.initiator = installSourceInfo.getInitiatingPackageName();
+                SigningInfo initiatorSignerInfo =
+                        installSourceInfo.getInitiatingPackageSigningInfo();
+                if (initiatorSignerInfo != null) {
+                    base.initiatorSignerDigests =
+                        computePackageSignerSha256Digests(initiatorSignerInfo);
+                }
+                base.installer = installSourceInfo.getInstallingPackageName();
+                base.originator = installSourceInfo.getOriginatingPackageName();
+            }
+
+            return results;
+        }
+
+        /**
+         * Perform basic measurement (i.e. content digest) on a given APK.
+         *
+         * @param apkPath The APK (or APEX, since it's also an APK) file to be measured.
+         * @return a {@link android.content.pm.Checksum} with preferred digest algorithm type and
+         *         the checksum.
+         */
+        private @Nullable Checksum measureApk(@NonNull String apkPath) {
+            // compute content digest
+            Map<Integer, byte[]> contentDigests = computeApkContentDigest(apkPath);
             if (contentDigests == null) {
-                Slog.d(TAG, "Failed to compute content digest for " + pkg.getBaseApkPath());
-                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
-                result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
-                return result;
+                Slog.d(TAG, "Failed to compute content digest for " + apkPath);
+                return new Checksum(0, new byte[] { -1 });
             }
 
             // in this iteration, we'll be supporting only 2 types of digests:
             // CHUNKED_SHA256 and CHUNKED_SHA512.
             // And only one of them will be available per package.
             if (contentDigests.containsKey(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) {
-                Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256;
-                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
-                result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
+                return new Checksum(
+                        Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256,
+                        contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256));
             } else if (contentDigests.containsKey(
                     ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) {
-                Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512;
-                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
-                result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
+                return new Checksum(
+                        Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512,
+                        contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512));
             } else {
                 // TODO(b/259423111): considering putting the raw values for the algorithm & digest
                 //  into the bundle to track potential other digest algorithms that may be in use
-                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
-                result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
+                return new Checksum(0, new byte[] { -1 });
             }
-            result.putBoolean(BUNDLE_PACKAGE_IS_APEX, packageState.isApex());
-
-            return result;
         }
 
 
@@ -330,7 +344,7 @@
             if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) {
                 // lastly measure all newly installed MBAs
                 List<IBinaryTransparencyService.AppInfo> allMbaInfo =
-                        collectAllMbaInfo(packagesMeasured);
+                        collectAllSilentInstalledMbaInfo(packagesMeasured);
                 for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) {
                     packagesMeasured.putBoolean(appInfo.packageName, true);
                     writeAppInfoToLog(appInfo);
@@ -356,18 +370,22 @@
                     continue;
                 }
 
-                Bundle apexMeasurement = measurePackage(packageState);
-                if (apexMeasurement == null) {
-                    Slog.w(TAG, "Skipping the missing APEX in " + packageState.getPath());
+                AndroidPackage pkg = packageState.getAndroidPackage();
+                if (pkg == null) {
+                    Slog.w(TAG, "Skipping the missing APK in " + pkg.getPath());
+                    continue;
+                }
+                Checksum apexChecksum = measureApk(pkg.getPath());
+                if (apexChecksum == null) {
+                    Slog.w(TAG, "Skipping the missing APEX in " + pkg.getPath());
                     continue;
                 }
 
                 var apexInfo = new IBinaryTransparencyService.ApexInfo();
                 apexInfo.packageName = packageState.getPackageName();
                 apexInfo.longVersion = packageState.getVersionCode();
-                apexInfo.digest = apexMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
-                apexInfo.digestAlgorithm =
-                        apexMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
+                apexInfo.digest = apexChecksum.getValue();
+                apexInfo.digestAlgorithm = apexChecksum.getType();
                 apexInfo.signerDigests =
                         computePackageSignerSha256Digests(packageState.getSigningInfo());
 
@@ -398,28 +416,16 @@
                 Slog.d(TAG, "Preload " + packageState.getPackageName() + " at "
                         + packageState.getPath() + " has likely been updated.");
 
-                Bundle packageMeasurement = measurePackage(packageState);
-                if (packageMeasurement == null) {
-                    Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
-                    return;
-                }
-
-                var appInfo = new IBinaryTransparencyService.AppInfo();
-                appInfo.packageName = packageState.getPackageName();
-                appInfo.longVersion = packageState.getVersionCode();
-                appInfo.digest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
-                appInfo.digestAlgorithm =
-                        packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
-                appInfo.signerDigests =
-                        computePackageSignerSha256Digests(packageState.getSigningInfo());
-                appInfo.mbaStatus = MBA_STATUS_UPDATED_PRELOAD;
-
-                results.add(appInfo);
+                List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo(
+                        packageState, MBA_STATUS_UPDATED_PRELOAD);
+                results.addAll(resultsForApp);
             });
             return results;
         }
 
-        public List<IBinaryTransparencyService.AppInfo> collectAllMbaInfo(Bundle packagesToSkip) {
+        @Override
+        public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo(
+                Bundle packagesToSkip) {
             var results = new ArrayList<IBinaryTransparencyService.AppInfo>();
             for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
                 if (packagesToSkip.containsKey(packageInfo.packageName)) {
@@ -433,42 +439,9 @@
                     continue;
                 }
 
-                Bundle packageMeasurement = measurePackage(packageState);
-                if (packageMeasurement == null) {
-                    Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
-                    continue;
-                }
-                if (DEBUG) {
-                    Slog.d(TAG,
-                            "Extracting InstallSourceInfo for " + packageState.getPackageName());
-                }
-                var appInfo = new IBinaryTransparencyService.AppInfo();
-                appInfo.packageName = packageState.getPackageName();
-                appInfo.longVersion = packageState.getVersionCode();
-                appInfo.digest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
-                appInfo.digestAlgorithm =
-                    packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
-                appInfo.signerDigests =
-                        computePackageSignerSha256Digests(packageState.getSigningInfo());
-                appInfo.mbaStatus = MBA_STATUS_NEW_INSTALL;
-
-                // Install source isn't currently available in PackageState (there's a TODO).
-                // Extract manually with another call.
-                InstallSourceInfo installSourceInfo = getInstallSourceInfo(
-                        packageState.getPackageName());
-                if (installSourceInfo != null) {
-                    appInfo.initiator = installSourceInfo.getInitiatingPackageName();
-                    SigningInfo initiatorSignerInfo =
-                            installSourceInfo.getInitiatingPackageSigningInfo();
-                    if (initiatorSignerInfo != null) {
-                        appInfo.initiatorSignerDigests =
-                                computePackageSignerSha256Digests(initiatorSignerInfo);
-                    }
-                    appInfo.installer = installSourceInfo.getInstallingPackageName();
-                    appInfo.originator = installSourceInfo.getOriginatingPackageName();
-                }
-
-                results.add(appInfo);
+                List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo(
+                        packageState, MBA_STATUS_NEW_INSTALL);
+                results.addAll(resultsForApp);
             }
             return results;
         }
diff --git a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
index 245db46..ae78dfe 100644
--- a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
@@ -40,7 +40,6 @@
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
-import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.SystemProperties;
@@ -166,36 +165,6 @@
     }
 
     @Test
-    public void getApexInfo_postInitialize_returnsValidEntries() throws RemoteException {
-        prepApexInfo();
-        List result = mTestInterface.getApexInfo();
-        Assert.assertNotNull("Apex info map should not be null", result);
-        // TODO(265244016): When PackageManagerInternal is a mock, it's harder to keep the
-        // `measurePackage` working in unit test. Disable it for now. We may need more refactoring
-        // or cover this in integration tests.
-        // Assert.assertFalse("Apex info map should not be empty", result.isEmpty());
-    }
-
-    @Test
-    public void getApexInfo_postInitialize_returnsActualApexs()
-            throws RemoteException, PackageManager.NameNotFoundException {
-        prepApexInfo();
-        List resultList = mTestInterface.getApexInfo();
-
-        PackageManager pm = mContext.getPackageManager();
-        Assert.assertNotNull(pm);
-        List<Bundle> castedResult = (List<Bundle>) resultList;
-        for (Bundle resultBundle : castedResult) {
-            String packageName = resultBundle.getString(
-                    BinaryTransparencyService.BUNDLE_PACKAGE_NAME);
-            Assert.assertNotNull("Package name for APEX should not be null", packageName);
-            Assert.assertTrue(packageName + "is not an APEX!",
-                    resultBundle.getBoolean(
-                            BinaryTransparencyService.BUNDLE_PACKAGE_IS_APEX));
-        }
-    }
-
-    @Test
     public void testCollectBiometricProperties_disablesFeature() {
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BIOMETRICS,
                 BinaryTransparencyService.KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION,
diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BaseInstallMultiple.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BaseInstallMultiple.java
new file mode 100644
index 0000000..3e94f25
--- /dev/null
+++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BaseInstallMultiple.java
@@ -0,0 +1,140 @@
+/*
+ * 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.transparency.test;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for invoking the install-multiple command via ADB. Subclass this for less typing:
+ *
+ * <code> private class InstallMultiple extends BaseInstallMultiple&lt;InstallMultiple&gt; { public
+ * InstallMultiple() { super(getDevice(), null); } } </code>
+ */
+/*package*/ class BaseInstallMultiple<T extends BaseInstallMultiple<?>> {
+
+    private final ITestDevice mDevice;
+    private final IBuildInfo mBuild;
+
+    private final List<String> mArgs = new ArrayList<>();
+    private final Map<File, String> mFileToRemoteMap = new HashMap<>();
+
+    /*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) {
+        mDevice = device;
+        mBuild = buildInfo;
+        addArg("-g");
+    }
+
+    T addArg(String arg) {
+        mArgs.add(arg);
+        return (T) this;
+    }
+
+    T addFile(String filename) throws FileNotFoundException {
+        return addFile(filename, filename);
+    }
+
+    T addFile(String filename, String remoteName) throws FileNotFoundException {
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
+        mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName);
+        return (T) this;
+    }
+
+    T inheritFrom(String packageName) {
+        addArg("-r");
+        addArg("-p " + packageName);
+        return (T) this;
+    }
+
+    void run() throws DeviceNotAvailableException {
+        run(true);
+    }
+
+    void runExpectingFailure() throws DeviceNotAvailableException {
+        run(false);
+    }
+
+    private void run(boolean expectingSuccess) throws DeviceNotAvailableException {
+        final ITestDevice device = mDevice;
+
+        // Create an install session
+        final StringBuilder cmd = new StringBuilder();
+        cmd.append("pm install-create");
+        for (String arg : mArgs) {
+            cmd.append(' ').append(arg);
+        }
+
+        String result = device.executeShellCommand(cmd.toString());
+        TestCase.assertTrue(result, result.startsWith("Success"));
+
+        final int start = result.lastIndexOf("[");
+        final int end = result.lastIndexOf("]");
+        int sessionId = -1;
+        try {
+            if (start != -1 && end != -1 && start < end) {
+                sessionId = Integer.parseInt(result.substring(start + 1, end));
+            }
+        } catch (NumberFormatException e) {
+            throw new IllegalStateException("Failed to parse install session: " + result);
+        }
+        if (sessionId == -1) {
+            throw new IllegalStateException("Failed to create install session: " + result);
+        }
+
+        // Push our files into session. Ideally we'd use stdin streaming,
+        // but ddmlib doesn't support it yet.
+        for (final Map.Entry<File, String> entry : mFileToRemoteMap.entrySet()) {
+            final File file = entry.getKey();
+            final String remoteName  = entry.getValue();
+            final String remotePath = "/data/local/tmp/" + file.getName();
+            if (!device.pushFile(file, remotePath)) {
+                throw new IllegalStateException("Failed to push " + file);
+            }
+
+            cmd.setLength(0);
+            cmd.append("pm install-write");
+            cmd.append(' ').append(sessionId);
+            cmd.append(' ').append(remoteName);
+            cmd.append(' ').append(remotePath);
+
+            result = device.executeShellCommand(cmd.toString());
+            TestCase.assertTrue(result, result.startsWith("Success"));
+        }
+
+        // Everything staged; let's pull trigger
+        cmd.setLength(0);
+        cmd.append("pm install-commit");
+        cmd.append(' ').append(sessionId);
+
+        result = device.executeShellCommand(cmd.toString());
+        if (expectingSuccess) {
+            TestCase.assertTrue(result, result.contains("Success"));
+        } else {
+            TestCase.assertFalse(result, result.contains("Success"));
+        }
+    }
+}
diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
index 6fe548f..b8e9a17 100644
--- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
+++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -81,6 +82,28 @@
     }
 
     @Test
+    public void testCollectAllSilentInstalledMbaInfo() throws Exception {
+        try {
+            new InstallMultiple()
+                .addFile("ApkVerityTestApp.apk")
+                .addFile("ApkVerityTestAppSplit.apk")
+                .run();
+            updatePreloadApp();
+            assertNotNull(getDevice().getAppPackageInfo("com.android.apkverity"));
+            assertNotNull(getDevice().getAppPackageInfo("com.android.egg"));
+
+            assertTrue(getDevice().setProperty("debug.transparency.bg-install-apps",
+                        "com.android.apkverity,com.android.egg"));
+            runDeviceTest("testCollectAllSilentInstalledMbaInfo");
+        } finally {
+            // No need to wait until job complete, since we can't verifying very meaningfully.
+            cancelPendingJob();
+            uninstallPackage("com.android.apkverity");
+            uninstallPackage("com.android.egg");
+        }
+    }
+
+    @Test
     public void testRebootlessApexUpdateTriggersJobScheduling() throws Exception {
         try {
             installRebootlessApex();
@@ -171,4 +194,13 @@
         result = getDevice().executeShellV2Command("pm install " + path);
         assertTrue(result.getStatus() == CommandStatus.SUCCESS);
     }
+
+    private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
+        InstallMultiple() {
+            super(getDevice(), getBuild());
+            // Needed since in getMockBackgroundInstalledPackages, getPackageInfo runs as the caller
+            // uid. This also makes it consistent with installPackage's behavior.
+            addArg("--force-queryable");
+        }
+    }
 }
diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
index 176bc28e..c087a85 100644
--- a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
+++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
@@ -36,6 +36,7 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.HexFormat;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 @RunWith(AndroidJUnit4.class)
@@ -111,4 +112,35 @@
         assertThat(updatedPreload.mbaStatus).isEqualTo(/* MBA_STATUS_UPDATED_PRELOAD */ 2);
         assertThat(updatedPreload.signerDigests).asList().containsNoneOf(null, "");
     }
+
+    @Test
+    public void testCollectAllSilentInstalledMbaInfo() {
+        // Action
+        var appInfoList = mBt.collectAllSilentInstalledMbaInfo(new Bundle());
+
+        // Verify
+        assertThat(appInfoList).isNotEmpty();  // because we just installed from the host side
+
+        var expectedAppNames = Set.of("com.android.apkverity", "com.android.egg");
+        var actualAppNames = appInfoList.stream().map((appInfo) -> appInfo.packageName)
+                .collect(Collectors.toList());
+        assertThat(actualAppNames).containsAtLeastElementsIn(expectedAppNames);
+
+        var actualSplitNames = new ArrayList<String>();
+        for (var appInfo : appInfoList) {
+            Log.d(TAG, "Received " + appInfo.packageName + " as a silent install");
+            if (expectedAppNames.contains(appInfo.packageName)) {
+                assertThat(appInfo.longVersion).isGreaterThan(0);
+                assertThat(appInfo.digestAlgorithm).isGreaterThan(0);
+                assertThat(appInfo.digest).isNotEmpty();
+                assertThat(appInfo.mbaStatus).isEqualTo(/* MBA_STATUS_NEW_INSTALL */ 3);
+                assertThat(appInfo.signerDigests).asList().containsNoneOf(null, "");
+
+                if (appInfo.splitName != null) {
+                    actualSplitNames.add(appInfo.splitName);
+                }
+            }
+        }
+        assertThat(actualSplitNames).containsExactly("feature_x");  // Name of ApkVerityTestAppSplit
+    }
 }