diff --git a/core/java/android/transparency/BinaryTransparencyManager.java b/core/java/android/transparency/BinaryTransparencyManager.java index d77bbcc836ecf..c18adfc86d549 100644 --- a/core/java/android/transparency/BinaryTransparencyManager.java +++ b/core/java/android/transparency/BinaryTransparencyManager.java @@ -66,24 +66,6 @@ public class BinaryTransparencyManager { } } - /** - * 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. * @@ -116,4 +98,21 @@ public class BinaryTransparencyManager { 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 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 e782aa7643a15..c8340acc5d8f8 100644 --- a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl +++ b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl @@ -28,8 +28,6 @@ import android.os.Bundle; interface IBinaryTransparencyService { String getSignedImageInfo(); - List getApexInfo(); - void recordMeasurementsForAllPackages(); parcelable ApexInfo { @@ -60,4 +58,5 @@ interface IBinaryTransparencyService { /** Test only */ List collectAllApexInfo(boolean includeTestOnly); List collectAllUpdatedPreloadInfo(in Bundle packagesToSkip); + List 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 b6a2a0eb3ac38..fc81675682b69 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.Intent; 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.os.IBinaryTransparencyService; 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 @@ public class BinaryTransparencyService extends SystemService { 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 @@ public class BinaryTransparencyService extends SystemService { return mVbmetaDigest; } - @Override - public List getApexInfo() { - List 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 @@ public class BinaryTransparencyService extends SystemService { 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} */ - private @Nullable Bundle measurePackage(PackageState packageState) { - Bundle result = new Bundle(); - + private @NonNull List 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(); + + // 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 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 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 @@ public class BinaryTransparencyService extends SystemService { if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) { // lastly measure all newly installed MBAs List allMbaInfo = - collectAllMbaInfo(packagesMeasured); + collectAllSilentInstalledMbaInfo(packagesMeasured); for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) { packagesMeasured.putBoolean(appInfo.packageName, true); writeAppInfoToLog(appInfo); @@ -356,18 +370,22 @@ public class BinaryTransparencyService extends SystemService { 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 @@ public class BinaryTransparencyService extends SystemService { 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 resultsForApp = collectAppInfo( + packageState, MBA_STATUS_UPDATED_PRELOAD); + results.addAll(resultsForApp); }); return results; } - public List collectAllMbaInfo(Bundle packagesToSkip) { + @Override + public List collectAllSilentInstalledMbaInfo( + Bundle packagesToSkip) { var results = new ArrayList(); for (PackageInfo packageInfo : getNewlyInstalledMbas()) { if (packagesToSkip.containsKey(packageInfo.packageName)) { @@ -433,42 +439,9 @@ public class BinaryTransparencyService extends SystemService { 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 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 245db46542483..ae78dfe624c65 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.FingerprintManager; 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; @@ -165,36 +164,6 @@ public class BinaryTransparencyServiceTest { BinaryTransparencyService.VBMETA_DIGEST_UNAVAILABLE), result); } - @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 castedResult = (List) 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, 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 0000000000000..3e94f25b5fc7d --- /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: + * + * private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { public + * InstallMultiple() { super(getDevice(), null); } } + */ +/*package*/ class BaseInstallMultiple> { + + private final ITestDevice mDevice; + private final IBuildInfo mBuild; + + private final List mArgs = new ArrayList<>(); + private final Map 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 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 6fe548fe0ba47..b8e9a1751775d 100644 --- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java +++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java @@ -18,6 +18,7 @@ package android.transparency.test; 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; @@ -80,6 +81,28 @@ public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test { } } + @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 { @@ -171,4 +194,13 @@ public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test { result = getDevice().executeShellV2Command("pm install " + path); assertTrue(result.getStatus() == CommandStatus.SUCCESS); } + + private class InstallMultiple extends BaseInstallMultiple { + 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 176bc28e8d6f6..c087a85da2a8d 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 org.junit.runner.RunWith; 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 @@ public class BinaryTransparencyTest { 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(); + 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 + } }