diff --git a/README.md b/README.md index 0d5bf542..64687317 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.8.0](https://github.com/google/bundletool/releases) +Latest release: [1.8.1](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index 08638716..dfdd98be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.8.0 +release_version = 1.8.1 diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java index 0ef692b4..9cc4eb17 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -163,8 +163,8 @@ public enum OutputFormat { private static final Flag KEY_ALIAS_FLAG = Flag.string("ks-key-alias"); private static final Flag KEYSTORE_PASSWORD_FLAG = Flag.password("ks-pass"); private static final Flag KEY_PASSWORD_FLAG = Flag.password("key-pass"); - private static final Flag MINIMUM_V3_SIGNING_API_VERSION_FLAG = - Flag.positiveInteger("min-v3-signing-api-version"); + private static final Flag MINIMUM_V3_ROTATION_API_VERSION_FLAG = + Flag.positiveInteger("min-v3-rotation-api-version"); // SourceStamp-related flags. private static final Flag CREATE_STAMP_FLAG = Flag.booleanFlag("create-stamp"); @@ -891,12 +891,12 @@ public static CommandHelp help() { .build()) .addFlag( FlagDescription.builder() - .setFlagName(MINIMUM_V3_SIGNING_API_VERSION_FLAG.getName()) + .setFlagName(MINIMUM_V3_ROTATION_API_VERSION_FLAG.getName()) .setExampleValue("30") .setOptional(true) .setDescription( - "The minimum API version for signing the generated APKs using V3 signature" - + " scheme.") + "The minimum API version for signing the generated APKs with rotation using V3" + + " signature scheme.") .build()) .addFlag( FlagDescription.builder() @@ -1120,7 +1120,7 @@ private static void populateSigningConfigurationFromFlags( Optional keyAlias = KEY_ALIAS_FLAG.getValue(flags); Optional keystorePassword = KEYSTORE_PASSWORD_FLAG.getValue(flags); Optional keyPassword = KEY_PASSWORD_FLAG.getValue(flags); - Optional minV3SigningApi = MINIMUM_V3_SIGNING_API_VERSION_FLAG.getValue(flags); + Optional minV3RotationApi = MINIMUM_V3_ROTATION_API_VERSION_FLAG.getValue(flags); if (keystorePath.isPresent() && keyAlias.isPresent()) { SignerConfig signerConfig = @@ -1129,7 +1129,7 @@ private static void populateSigningConfigurationFromFlags( SigningConfiguration.Builder builder = SigningConfiguration.builder() .setSignerConfig(signerConfig) - .setMinimumV3RotationApiVersion(minV3SigningApi); + .setMinimumV3RotationApiVersion(minV3RotationApi); populateLineageFromFlags(builder, flags); buildApksCommand.setSigningConfiguration(builder.build()); } else if (keystorePath.isPresent() && !keyAlias.isPresent()) { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java index d5080bdc..516e788b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java @@ -38,6 +38,7 @@ import com.android.tools.build.bundletool.device.Device; import com.android.tools.build.bundletool.device.Device.InstallOptions; import com.android.tools.build.bundletool.device.DeviceAnalyzer; +import com.android.tools.build.bundletool.device.LocalTestingPathResolver; import com.android.tools.build.bundletool.flags.Flag; import com.android.tools.build.bundletool.flags.ParsedFlags; import com.android.tools.build.bundletool.io.TempDirectory; @@ -50,6 +51,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.protobuf.Int32Value; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -215,9 +217,30 @@ public void execute() { if (!filesToPush.isEmpty()) { pushFiles(filesToPush, toc, adbRunner); } + if (toc.getLocalTestingInfo().getEnabled()) { + cleanUpEmulatedSplits(adbRunner, toc); + } } } + private void cleanUpEmulatedSplits(AdbRunner adbRunner, BuildApksResult toc) { + adbRunner.run( + device -> { + try { + device.removeRemotePath( + LocalTestingPathResolver.getLocalTestingWorkingDir(toc.getPackageName()), + Optional.of(toc.getPackageName()), + getTimeout()); + } catch (IOException e) { + System.err.println( + "Failed to remove working directory with local testing splits. Your app might" + + " still have been installed correctly but have previous version of" + + " dynamic feature modules. If you see legacy versions of dynamic feature" + + " modules installed try to uninstall and install the app again."); + } + }); + } + /** Extracts the apks that will be installed. */ private ImmutableList getApksToInstall( BuildApksResult toc, DeviceSpec deviceSpec, Path output) { diff --git a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java index 9a19776a..b8653460 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DdmlibDevice.java @@ -19,6 +19,9 @@ import static com.android.tools.build.bundletool.device.LocalTestingPathResolver.resolveLocalTestingPath; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Arrays.stream; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.DdmPreferences; @@ -34,6 +37,7 @@ import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; import com.google.errorprone.annotations.FormatMethod; import java.io.File; import java.io.IOException; @@ -41,12 +45,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Clock; -import java.util.Arrays; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; /** Ddmlib-backed implementation of the {@link Device}. */ public class DdmlibDevice extends Device { + private static final String DENSITY_OUTPUT_PREFIX = "Physical density:"; private final IDevice device; private final Clock clock; @@ -84,7 +89,40 @@ public ImmutableList getAbis() { @Override public int getDensity() { - return device.getDensity(); + int density = device.getDensity(); + if (density != -1) { + return density; + } + // This might be a case when ddmlib is unable to retrieve density via reading properties. + // For example this happens on Android S emulator. + try { + int[] parsedDensityFromShell = new int[] {-1}; + device.executeShellCommand( + "wm density", + new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + stream(lines) + .filter(string -> string.startsWith(DENSITY_OUTPUT_PREFIX)) + .map(string -> string.substring(DENSITY_OUTPUT_PREFIX.length()).trim()) + .map(Ints::tryParse) + .forEach(density -> parsedDensityFromShell[0] = density != null ? density : -1); + } + + @Override + public boolean isCancelled() { + return false; + } + }, + /* maxTimeToOutputResponse= */ 1, + MINUTES); + return parsedDensityFromShell[0]; + } catch (TimeoutException + | AdbCommandRejectedException + | ShellCommandUnresponsiveException + | IOException e) { + return -1; + } } @Override @@ -101,14 +139,13 @@ public Optional getProperty(String propertyName) { public ImmutableList getDeviceFeatures() { return deviceFeaturesParser.parse( new AdbShellCommandTask(this, DEVICE_FEATURES_COMMAND) - .execute(ADB_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + .execute(ADB_TIMEOUT_MS, MILLISECONDS)); } @Override public ImmutableList getGlExtensions() { return glExtensionsParser.parse( - new AdbShellCommandTask(this, GL_EXTENSIONS_COMMAND) - .execute(ADB_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + new AdbShellCommandTask(this, GL_EXTENSIONS_COMMAND).execute(ADB_TIMEOUT_MS, MILLISECONDS)); } @Override @@ -141,7 +178,7 @@ public void installApks(ImmutableList apks, InstallOptions installOptions) installOptions.getAllowReinstall(), extraArgs.build(), installOptions.getTimeout().toMillis(), - TimeUnit.MILLISECONDS); + MILLISECONDS); } else { device.installPackage( Iterables.getOnlyElement(apkFiles).toString(), @@ -177,10 +214,14 @@ public void push(ImmutableList files, PushOptions pushOptions) { // ... and recreate it, making sure the destination dir is empty. // We don't want splits from previous runs in the directory. // There isn't a nice way to test if dir is empty in shell, but rmdir will return error - commandExecutor.executeAndPrint( - "mkdir -p %s && rmdir %s && mkdir -p %s", splitsPath, splitsPath, splitsPath); + commandExecutor.executeAndPrint("mkdir -p %s && rmdir %1$s && mkdir -p %1$s", splitsPath); pushFiles(commandExecutor, splitsPath, files); + + // Fix permission issue for devices on Android S. + if (device.getVersion().getApiLevel() >= 31 || device.getVersion().isPreview()) { + commandExecutor.executeAndPrint("chmod 775 %s", splitsPath); + } } catch (IOException | TimeoutException | SyncException @@ -233,8 +274,23 @@ public Path syncPackageToDevice(Path localFilePath) } @Override - public void removeRemotePackage(Path remoteFilePath) throws InstallException { - device.removeRemotePackage(remoteFilePath.toString()); + public void removeRemotePath( + String remoteFilePath, Optional runAsPackageName, Duration timeout) + throws IOException { + RemoteCommandExecutor executor = + new RemoteCommandExecutor(this, timeout.toMillis(), System.err); + try { + if (runAsPackageName.isPresent()) { + executor.executeAndPrint("run-as %s rm -rf %s", runAsPackageName.get(), remoteFilePath); + } else { + executor.executeAndPrint("rm -rf %s", remoteFilePath); + } + } catch (TimeoutException + | AdbCommandRejectedException + | ShellCommandUnresponsiveException + | IOException e) { + throw new IOException(String.format("Failed to remove '%s'", remoteFilePath), e); + } } @Override @@ -291,7 +347,7 @@ private void executeAndPrint(String commandFormat, String... args) // ExecuteShellCommand would only tell us about ADB errors, and NOT the actual shell commands // We need another way to check exit values of the commands we run. // By adding " && echo OK" we can make sure "OK" is printed if the cmd executed successfully. - device.executeShellCommand(command + " && echo OK", receiver, timeout, TimeUnit.MILLISECONDS); + device.executeShellCommand(command + " && echo OK", receiver, timeout, MILLISECONDS); if (!"OK".equals(lastOutputLine)) { throw new IOException("ADB command failed."); } @@ -309,7 +365,7 @@ static String escapeAndSingleQuote(String string) { @FormatMethod static String formatCommandWithArgs(String command, String... args) { return String.format( - command, Arrays.stream(args).map(RemoteCommandExecutor::escapeAndSingleQuote).toArray()); + command, stream(args).map(RemoteCommandExecutor::escapeAndSingleQuote).toArray()); } } diff --git a/src/main/java/com/android/tools/build/bundletool/device/Device.java b/src/main/java/com/android/tools/build/bundletool/device/Device.java index 322f0fd5..26b91973 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/Device.java +++ b/src/main/java/com/android/tools/build/bundletool/device/Device.java @@ -19,7 +19,6 @@ import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice.DeviceState; import com.android.ddmlib.IShellOutputReceiver; -import com.android.ddmlib.InstallException; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.SyncException; import com.android.ddmlib.TimeoutException; @@ -70,7 +69,9 @@ public abstract void executeShellCommand( public abstract Path syncPackageToDevice(Path localFilePath) throws TimeoutException, AdbCommandRejectedException, SyncException, IOException; - public abstract void removeRemotePackage(Path remoteFilePath) throws InstallException; + public abstract void removeRemotePath( + String remoteFilePath, Optional runAsPackageName, Duration timeout) + throws IOException; public abstract void pull(ImmutableList files); diff --git a/src/main/java/com/android/tools/build/bundletool/device/LocalTestingPathResolver.java b/src/main/java/com/android/tools/build/bundletool/device/LocalTestingPathResolver.java index c5dfc840..462eb2e2 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/LocalTestingPathResolver.java +++ b/src/main/java/com/android/tools/build/bundletool/device/LocalTestingPathResolver.java @@ -40,4 +40,8 @@ public static String resolveLocalTestingPath(String localTestPath, Optional getSupportsGlTextures() { } private static boolean isSdkCodename(String sdkVersion) { + if (sdkVersion.isEmpty()) { + return false; + } // Codename version can be of the form "[codename]" or "[codename].[fingerprint]". - return !sdkVersion.isEmpty() - && Range.closed('A', 'Z').contains(sdkVersion.charAt(0)) - && (sdkVersion.length() == 1 || '.' == sdkVersion.charAt(1)); + int dotIndex = sdkVersion.indexOf('.'); + String codename = dotIndex != -1 ? sdkVersion.substring(0, dotIndex) : sdkVersion; + return Ints.tryParse(codename) == null; } public boolean hasApplicationElement() { diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java index 17cc8155..3d6ec364 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "1.8.0"; + private static final String CURRENT_VERSION = "1.8.1"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/transparency/ApkTransparencyCheckUtils.java b/src/main/java/com/android/tools/build/bundletool/transparency/ApkTransparencyCheckUtils.java index c9799007..316fa483 100644 --- a/src/main/java/com/android/tools/build/bundletool/transparency/ApkTransparencyCheckUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/transparency/ApkTransparencyCheckUtils.java @@ -89,6 +89,8 @@ public static TransparencyCheckResult checkTransparency(ImmutableList devi CodeTransparency codeTransparencyMetadata = CodeTransparencyFactory.parseFrom(jws.getUnverifiedPayload()); + CodeTransparencyVersion.checkVersion(codeTransparencyMetadata); + ImmutableSet pathsToModifiedFiles = getModifiedFiles(codeTransparencyMetadata, deviceSpecificApks); result.fileContentsVerified(pathsToModifiedFiles.isEmpty()); @@ -166,7 +168,7 @@ private static ImmutableSet getModifiedNativeLibraries( private static ImmutableSet getDexFiles(CodeTransparency codeTransparency) { return codeTransparency.getCodeRelatedFileList().stream() - .filter(codeRelatedFile -> codeRelatedFile.getType().equals(CodeRelatedFile.Type.DEX)) + .filter(ApkTransparencyCheckUtils::isDexFile) .map(CodeRelatedFile::getSha256) .collect(toImmutableSet()); } @@ -184,6 +186,14 @@ private static boolean isDexFile(ZipEntry zipEntry) { return zipEntry.getName().endsWith(".dex"); } + private static boolean isDexFile(CodeRelatedFile codeRelatedFile) { + return codeRelatedFile.getType().equals(CodeRelatedFile.Type.DEX) + // Code transparency files generated using Bundletool with version older than + // 1.8.1 do not have type field set for dex files. + || (codeRelatedFile.getType().equals(CodeRelatedFile.Type.TYPE_UNSPECIFIED) + && codeRelatedFile.getPath().endsWith(".dex")); + } + private static boolean isNativeLibrary(ZipEntry zipEntry) { return zipEntry.getName().endsWith(".so"); } diff --git a/src/main/java/com/android/tools/build/bundletool/transparency/BundleTransparencyCheckUtils.java b/src/main/java/com/android/tools/build/bundletool/transparency/BundleTransparencyCheckUtils.java index 98568a14..23a20a06 100644 --- a/src/main/java/com/android/tools/build/bundletool/transparency/BundleTransparencyCheckUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/transparency/BundleTransparencyCheckUtils.java @@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.android.bundle.CodeTransparencyOuterClass.CodeRelatedFile; +import com.android.bundle.CodeTransparencyOuterClass.CodeTransparency; import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; @@ -81,9 +82,13 @@ public static TransparencyCheckResult checkTransparency( .transparencyKeyCertificateFingerprint( CodeTransparencyCryptoUtils.getCertificateFingerprint(jws)); + CodeTransparency parsedTransparencyFile = + CodeTransparencyFactory.parseFrom(jws.getUnverifiedPayload()); + CodeTransparencyVersion.checkVersion(parsedTransparencyFile); + MapDifference difference = Maps.difference( - getCodeRelatedFilesFromTransparencyMetadata(jws), + getCodeRelatedFilesFromParsedTransparencyFile(parsedTransparencyFile), getCodeRelatedFilesFromBundle(bundle)); result.fileContentsVerified(difference.areEqual()); if (!difference.areEqual()) { @@ -92,14 +97,23 @@ public static TransparencyCheckResult checkTransparency( return result.build(); } - private static ImmutableMap getCodeRelatedFilesFromTransparencyMetadata( - JsonWebSignature signedTransparencyFile) { - return CodeTransparencyFactory.parseFrom(signedTransparencyFile.getUnverifiedPayload()) - .getCodeRelatedFileList() - .stream() + private static ImmutableMap + getCodeRelatedFilesFromParsedTransparencyFile(CodeTransparency parsedTransparencyFile) { + return parsedTransparencyFile.getCodeRelatedFileList().stream() + .map(BundleTransparencyCheckUtils::addTypeToDexCodeRelatedFiles) .collect(toImmutableMap(CodeRelatedFile::getPath, codeRelatedFile -> codeRelatedFile)); } + // Code transparency files generated using Bundletool with version older than + // 1.8.1 do not have type field set for dex files. + private static CodeRelatedFile addTypeToDexCodeRelatedFiles(CodeRelatedFile codeRelatedFile) { + if (codeRelatedFile.getType().equals(CodeRelatedFile.Type.TYPE_UNSPECIFIED) + && codeRelatedFile.getPath().endsWith(".dex")) { + return codeRelatedFile.toBuilder().setType(CodeRelatedFile.Type.DEX).build(); + } + return codeRelatedFile; + } + private static ImmutableMap getCodeRelatedFilesFromBundle( AppBundle bundle) { return CodeTransparencyFactory.createCodeTransparencyMetadata(bundle) diff --git a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java index e4bef6b5..d9f63b42 100644 --- a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java +++ b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java @@ -43,7 +43,10 @@ public static CodeTransparency createCodeTransparencyMetadata(AppBundle bundle) .map(CodeTransparencyFactory::createCodeRelatedFile) .sorted(Comparator.comparing(CodeRelatedFile::getPath)) .collect(toImmutableList()); - return CodeTransparency.newBuilder().addAllCodeRelatedFile(codeRelatedFiles).build(); + return CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) + .addAllCodeRelatedFile(codeRelatedFiles) + .build(); } /** Returns {@link CodeTransparency} parsed from transparency file JSON payload. */ diff --git a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyVersion.java b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyVersion.java new file mode 100644 index 00000000..c61d2ee1 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyVersion.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 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.tools.build.bundletool.transparency; + +import com.android.bundle.CodeTransparencyOuterClass.CodeTransparency; + +/** Class representing code transparency version. */ +public final class CodeTransparencyVersion { + + // Code transparency files created before the version field was introduced will be treated by + // Bundletool as having version 0. + private static final int FIRST_VERSION = 0; + private static final int CURRENT_VERSION = 1; + + /** Returns current code transparency version used by Bundletool. */ + public static int getCurrentVersion() { + return CURRENT_VERSION; + } + + /** + * Throws a exception if {@code codeTransparency} has version that is not supported by Bundletool. + */ + public static void checkVersion(CodeTransparency codeTransparency) { + if (!isSupportedVersion(codeTransparency.getVersion())) { + throw new IllegalStateException("Code transparency file has unsupported version."); + } + } + + private static boolean isSupportedVersion(int version) { + return FIRST_VERSION <= version && version <= CURRENT_VERSION; + } + + private CodeTransparencyVersion() {} +} diff --git a/src/main/proto/app_dependencies.proto b/src/main/proto/app_dependencies.proto index 9926edce..7546c60f 100644 --- a/src/main/proto/app_dependencies.proto +++ b/src/main/proto/app_dependencies.proto @@ -2,6 +2,8 @@ syntax = "proto2"; package android.bundle; +import "google/protobuf/wrappers.proto"; + option java_package = "com.android.bundle"; // Lists the dependencies of an application. @@ -14,6 +16,9 @@ message AppDependencies { // List of direct dependencies per bundle module. repeated ModuleDependencies module_dependencies = 3; + + // List of repositories where dependencies were found. + repeated Repository repositories = 4; } // List of dependencies of a given library. @@ -45,6 +50,10 @@ message Library { } optional Digests digests = 2; + + // Repository from which the artifact was retrieved (if known). + // Index is from pool of repositories defined in AppDependencies. + optional google.protobuf.Int32Value repo_index = 3; } message MavenLibrary { @@ -54,3 +63,22 @@ message MavenLibrary { optional string classifier = 4; optional string version = 5; } + +// A repository for resolving artifacts and metadata. +message Repository { + // The type of the repository, and any type-specific configuration info. + oneof repo_oneof { + MavenRepo maven_repo = 1; + IvyRepo ivy_repo = 2; + } +} + +message MavenRepo { + // The root url for the repository. + optional string url = 1; +} + +message IvyRepo { + // The root url for the repository. + optional string url = 1; +} diff --git a/src/main/proto/code_transparency.proto b/src/main/proto/code_transparency.proto index bd58c560..f79b948e 100644 --- a/src/main/proto/code_transparency.proto +++ b/src/main/proto/code_transparency.proto @@ -8,12 +8,17 @@ option java_package = "com.android.bundle"; message CodeTransparency { // List of code-related files in the bundle. repeated CodeRelatedFile code_related_file = 1; + + // Version of code transparency file format. + // Required. + int32 version = 2; } message CodeRelatedFile { enum Type { - DEX = 0; + TYPE_UNSPECIFIED = 0; NATIVE_LIBRARY = 1; + DEX = 2; } // Path to file in the bundle. // Required. diff --git a/src/test/java/com/android/tools/build/bundletool/commands/AddTransparencyCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/AddTransparencyCommandTest.java index 538baf9b..6c0ff233 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/AddTransparencyCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/AddTransparencyCommandTest.java @@ -49,6 +49,7 @@ import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.CertificateFactory; import com.android.tools.build.bundletool.transparency.CodeTransparencyFactory; +import com.android.tools.build.bundletool.transparency.CodeTransparencyVersion; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.hash.Hashing; @@ -89,10 +90,12 @@ public final class AddTransparencyCommandTest { private static final String DEX2 = "dex/classes2.dex"; private static final String NATIVE_LIB_PATH1 = "lib/arm64-v8a/libnative.so"; private static final String NATIVE_LIB_PATH2 = "lib/armeabi-v7a/libnative.so"; + private static final String NATIVE_LIB_LARGE = "lib/armeabi-v7a/libnative_large.so"; private static final byte[] FILE_CONTENT_DEX1 = new byte[] {1, 2, 3}; private static final byte[] FILE_CONTENT_DEX2 = new byte[] {2, 3, 4}; private static final byte[] FILE_CONTENT_NATIVE_LIB1 = new byte[] {3, 4, 5}; private static final byte[] FILE_CONTENT_NATIVE_LIB2 = new byte[] {4, 5, 6}; + private static final byte[] FILE_CONTENT_NATIVE_LARGE = new byte[4000]; private static final String RES_FILE = "res/image.png"; private static final String NON_CODE_FILE_IN_LIB_DIRECTORY = "lib/wrap.sh"; private static final String KEYSTORE_PASSWORD = "keystore-password"; @@ -113,6 +116,7 @@ public final class AddTransparencyCommandTest { private String fileHashDex2; private String fileHashNativeLib1; private String fileHashNativeLib2; + private String fileHashNativeLibLarge; @Before public void setUp() throws Exception { @@ -131,6 +135,8 @@ public void setUp() throws Exception { ByteSource.wrap(FILE_CONTENT_NATIVE_LIB1).hash(Hashing.sha256()).toString(); fileHashNativeLib2 = ByteSource.wrap(FILE_CONTENT_NATIVE_LIB2).hash(Hashing.sha256()).toString(); + fileHashNativeLibLarge = + ByteSource.wrap(FILE_CONTENT_NATIVE_LARGE).hash(Hashing.sha256()).toString(); } @Test @@ -914,6 +920,7 @@ private static BundleModule addCodeFilesToBundleModule( .addFile(DEX2, FILE_CONTENT_DEX2) .addFile(NATIVE_LIB_PATH1, FILE_CONTENT_NATIVE_LIB1) .addFile(NATIVE_LIB_PATH2, FILE_CONTENT_NATIVE_LIB2) + .addFile(NATIVE_LIB_LARGE, FILE_CONTENT_NATIVE_LARGE) // 2 files below are not code related and should not be included in the transparency file. .addFile(RES_FILE) .addFile(NON_CODE_FILE_IN_LIB_DIRECTORY) @@ -921,7 +928,8 @@ private static BundleModule addCodeFilesToBundleModule( } private CodeTransparency expectedTransparencyProto() { - CodeTransparency.Builder transparencyBuilder = CodeTransparency.newBuilder(); + CodeTransparency.Builder transparencyBuilder = + CodeTransparency.newBuilder().setVersion(CodeTransparencyVersion.getCurrentVersion()); addCodeFilesToTransparencyProto(transparencyBuilder, BASE_MODULE); addCodeFilesToTransparencyProto(transparencyBuilder, FEATURE_MODULE1); addCodeFilesToTransparencyProto(transparencyBuilder, FEATURE_MODULE2); @@ -958,6 +966,13 @@ private void addCodeFilesToTransparencyProto( .setType(CodeRelatedFile.Type.NATIVE_LIBRARY) .setApkPath(NATIVE_LIB_PATH2) .setSha256(fileHashNativeLib2) + .build()) + .addCodeRelatedFile( + CodeRelatedFile.newBuilder() + .setPath(moduleName + "/" + NATIVE_LIB_LARGE) + .setType(CodeRelatedFile.Type.NATIVE_LIBRARY) + .setApkPath(NATIVE_LIB_LARGE) + .setSha256(fileHashNativeLibLarge) .build()); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java index 2754e7c5..c3ae9f2c 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -1309,7 +1309,7 @@ public void populateMinV3SigningApi() { "--ks-key-alias=" + KEY_ALIAS, "--ks-pass=pass:" + KEYSTORE_PASSWORD, "--key-pass=pass:" + KEY_PASSWORD, - "--min-v3-signing-api-version=" + minV3Api), + "--min-v3-rotation-api-version=" + minV3Api), new PrintStream(output), systemEnvironmentProvider, fakeAdbServer); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java index 4d23bac4..6fe92612 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/CheckTransparencyCommandTest.java @@ -53,6 +53,7 @@ import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; import com.android.tools.build.bundletool.testing.TestModule; import com.android.tools.build.bundletool.transparency.CodeTransparencyCryptoUtils; +import com.android.tools.build.bundletool.transparency.CodeTransparencyVersion; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.hash.Hashing; @@ -542,6 +543,38 @@ public void execute_apkMode_wrongInputFileFormat() { assertThat(e).hasMessageThat().contains("expected to have '.zip' extension."); } + @Test + public void bundleMode_unsupportedCodeTransparencyVersion() throws Exception { + String serializedJws = + createJwsToken( + CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion() + 1) + .build(), + transparencyKeyCertificate, + transparencyPrivateKey); + AppBundleBuilder appBundle = + new AppBundleBuilder() + .addModule("base", module -> module.setManifest(androidManifest("com.test.app"))) + .addMetadataFile( + BundleMetadata.BUNDLETOOL_NAMESPACE, + BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME, + CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())); + new AppBundleSerializer().writeToDisk(appBundle.build(), bundlePath); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + Throwable e = + assertThrows( + IllegalStateException.class, + () -> + CheckTransparencyCommand.builder() + .setMode(Mode.BUNDLE) + .setBundlePath(bundlePath) + .setTransparencyKeyCertificate(transparencyKeyCertificate) + .build() + .checkTransparency(new PrintStream(outputStream))); + assertThat(e).hasMessageThat().contains("Code transparency file has unsupported version."); + } + @Test public void bundleMode_unsupportedSignatureAlgorithm() throws Exception { String serializedJws = @@ -576,6 +609,44 @@ public void bundleMode_unsupportedSignatureAlgorithm() throws Exception { @Test public void bundleMode_transparencyVerified_transparencyKeyCertificateProvidedByUser() throws Exception { + String serializedJws = + createJwsToken( + CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) + .build(), + transparencyKeyCertificate, + transparencyPrivateKey); + AppBundleBuilder appBundle = + new AppBundleBuilder() + .addModule("base", module -> module.setManifest(androidManifest("com.test.app"))) + .addMetadataFile( + BundleMetadata.BUNDLETOOL_NAMESPACE, + BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME, + CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())); + new AppBundleSerializer().writeToDisk(appBundle.build(), bundlePath); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + CheckTransparencyCommand.builder() + .setMode(Mode.BUNDLE) + .setBundlePath(bundlePath) + .setTransparencyKeyCertificate(transparencyKeyCertificate) + .build() + .checkTransparency(new PrintStream(outputStream)); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output).contains("No APK present. APK signature was not checked."); + assertThat(output) + .contains( + "Code transparency signature verified for the provided code transparency key" + + " certificate."); + assertThat(output) + .contains( + "Code transparency verified: code related file contents match the code transparency" + + " file."); + } + + @Test + public void bundleMode_transparencyVerified_codeTransparencyVersionNotSet() throws Exception { String serializedJws = createJwsToken( CodeTransparency.getDefaultInstance(), @@ -614,7 +685,9 @@ public void bundleMode_transparencyVerified_transparencyKeyCertificateProvidedBy public void bundleMode_verificationFailed_badCertificateProvidedByUser() throws Exception { String serializedJws = createJwsToken( - CodeTransparency.getDefaultInstance(), + CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) + .build(), transparencyKeyCertificate, transparencyPrivateKey); AppBundleBuilder appBundle = @@ -689,6 +762,68 @@ public void bundleMode_verificationFailed_transparencyKeyCertificateNotProvidedB .contains("Verification failed because code transparency signature is invalid."); } + @Test + public void apkMode_transparencyVerified_unsupportedCodeTransparencyVersion() throws Exception { + Path apkPath = tmpDir.resolve("universal.apk"); + Path zipOfApksPath = tmpDir.resolve("apks.zip"); + String dexFileName = "classes.dex"; + byte[] dexFileContents = new byte[] {1, 2, 3}; + String serializedJws = + createJwsToken( + CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion() + 1) + .addCodeRelatedFile( + CodeRelatedFile.newBuilder() + .setType(CodeRelatedFile.Type.DEX) + .setPath("base/dex/" + dexFileName) + .setSha256( + ByteSource.wrap(dexFileContents).hash(Hashing.sha256()).toString()) + .build()) + .build(), + transparencyKeyCertificate, + transparencyPrivateKey); + ModuleSplit baseModuleSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.app"))) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .setMasterSplit(true) + .addEntry( + ModuleEntry.builder() + .setPath(ZipPath.create("").resolve(dexFileName)) + .setContent(ByteSource.wrap(dexFileContents)) + .build()) + .addEntry( + ModuleEntry.builder() + .setPath( + ZipPath.create("META-INF") + .resolve(BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME)) + .setContent( + CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) + .build()) + .build(); + apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + ZipBuilder zipBuilder = + new ZipBuilder() + .addFileWithContent( + ZipPath.create("universal.apk"), + ByteString.readFrom(Files.newInputStream(apkPath)).toByteArray()); + zipBuilder.writeTo(zipOfApksPath); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + Throwable e = + assertThrows( + IllegalStateException.class, + () -> + CheckTransparencyCommand.builder() + .setMode(Mode.APK) + .setApkZipPath(zipOfApksPath) + .build() + .checkTransparency(new PrintStream(outputStream))); + assertThat(e).hasMessageThat().contains("Code transparency file has unsupported version."); + } + @Test public void apkMode_transparencyVerified_transparencyKeyCertificateNotProvidedByUser() throws Exception { @@ -699,6 +834,7 @@ public void apkMode_transparencyVerified_transparencyKeyCertificateNotProvidedBy String serializedJws = createJwsToken( CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) .addCodeRelatedFile( CodeRelatedFile.newBuilder() .setType(CodeRelatedFile.Type.DEX) @@ -773,6 +909,7 @@ public void apkMode_transparencyVerified_apkSigningKeyCertificateProvidedByUser( String serializedJws = createJwsToken( CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) .addCodeRelatedFile( CodeRelatedFile.newBuilder() .setType(CodeRelatedFile.Type.DEX) @@ -835,6 +972,80 @@ public void apkMode_transparencyVerified_apkSigningKeyCertificateProvidedByUser( + " file."); } + @Test + public void apkMode_transparencyVerified_unspecifiedTypeForDexFiles() throws Exception { + Path apkPath = tmpDir.resolve("universal.apk"); + Path zipOfApksPath = tmpDir.resolve("apks.zip"); + String dexFileName = "classes.dex"; + byte[] dexFileContents = new byte[] {1, 2, 3}; + String serializedJws = + createJwsToken( + CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) + .addCodeRelatedFile( + CodeRelatedFile.newBuilder() + .setType(CodeRelatedFile.Type.TYPE_UNSPECIFIED) + .setPath("base/dex/" + dexFileName) + .setSha256( + ByteSource.wrap(dexFileContents).hash(Hashing.sha256()).toString()) + .build()) + .build(), + transparencyKeyCertificate, + transparencyPrivateKey); + ModuleSplit baseModuleSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.app"))) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .setMasterSplit(true) + .addEntry( + ModuleEntry.builder() + .setPath(ZipPath.create("").resolve(dexFileName)) + .setContent(ByteSource.wrap(dexFileContents)) + .build()) + .addEntry( + ModuleEntry.builder() + .setPath( + ZipPath.create("META-INF") + .resolve(BundleMetadata.TRANSPARENCY_SIGNED_FILE_NAME)) + .setContent( + CharSource.wrap(serializedJws).asByteSource(Charset.defaultCharset())) + .build()) + .build(); + apkSerializerHelper.writeToZipFile(baseModuleSplit, apkPath); + ZipBuilder zipBuilder = + new ZipBuilder() + .addFileWithContent( + ZipPath.create("universal.apk"), + ByteString.readFrom(Files.newInputStream(apkPath)).toByteArray()); + zipBuilder.writeTo(zipOfApksPath); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + CheckTransparencyCommand.builder() + .setMode(Mode.APK) + .setApkZipPath(zipOfApksPath) + .build() + .checkTransparency(new PrintStream(outputStream)); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .contains( + "APK signature is valid. SHA-256 fingerprint of the apk signing key certificate (must" + + " be compared with the developer's public key manually): " + + CodeTransparencyCryptoUtils.getCertificateFingerprint(apkSigningKeyCertificate)); + assertThat(output) + .contains( + "Code transparency signature is valid. SHA-256 fingerprint of the code transparency key" + + " certificate (must be compared with the developer's public key manually): " + + CodeTransparencyCryptoUtils.getCertificateFingerprint( + (JsonWebSignature) JsonWebSignature.fromCompactSerialization(serializedJws))); + assertThat(output) + .contains( + "Code transparency verified: code related file contents match the code transparency" + + " file."); + } + @Test public void apkMode_verificationFailed_apkSigningKeyCertificateMismatch() throws Exception { Path apkPath = tmpDir.resolve("universal.apk"); @@ -844,6 +1055,7 @@ public void apkMode_verificationFailed_apkSigningKeyCertificateMismatch() throws String serializedJws = createJwsToken( CodeTransparency.newBuilder() + .setVersion(CodeTransparencyVersion.getCurrentVersion()) .addCodeRelatedFile( CodeRelatedFile.newBuilder() .setType(CodeRelatedFile.Type.DEX) diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java index 39a046cf..03e786b7 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java @@ -44,6 +44,7 @@ import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Commands.AssetModuleMetadata; @@ -61,6 +62,7 @@ import com.android.bundle.Targeting.VariantTargeting; import com.android.ddmlib.IDevice.DeviceState; import com.android.tools.build.bundletool.device.AdbServer; +import com.android.tools.build.bundletool.device.LocalTestingPathResolver; import com.android.tools.build.bundletool.flags.FlagParser; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; @@ -925,6 +927,7 @@ public void localTestingMode_defaultModules( List installedApks = new ArrayList<>(); List pushedFiles = new ArrayList<>(); + List clearedPathes = new ArrayList<>(); FakeDevice fakeDevice = FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); AdbServer adbServer = @@ -939,6 +942,12 @@ public void localTestingMode_defaultModules( assertThat(pushOptions.getTimeout()).isEqualTo(timeout); pushedFiles.addAll(files); }); + fakeDevice.setRemoveRemotePathSideEffect( + (remotePath, runAs, removeTimeout) -> { + assertThat(removeTimeout).isEqualTo(timeout); + assertThat(runAs).hasValue(PKG_NAME); + clearedPathes.add(remotePath); + }); InstallApksCommand.builder() .setApksArchivePath(apksFile) @@ -966,6 +975,8 @@ public void localTestingMode_defaultModules( installTimeFeaturePlApk.toString(), onDemandFeatureMasterApk.toString(), onDemandAssetMasterApk.toString()); + assertThat(clearedPathes) + .containsExactly(LocalTestingPathResolver.getLocalTestingWorkingDir(PKG_NAME)); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java b/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java index 4b85f41e..c8d3efb0 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/DdmlibDeviceTest.java @@ -39,9 +39,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; @@ -123,6 +125,7 @@ public void allowTestOnly() throws Exception { @Test public void pushFiles_targetLocation() throws Exception { String destinationPath = "/destination/path"; + when(mockDevice.getVersion()).thenReturn(new AndroidVersion(VersionCodes.KITKAT)); DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); mockAdbShellCommand(String.format("rm -rf '%s' && echo OK", destinationPath), "OK\n"); @@ -144,10 +147,43 @@ public void pushFiles_targetLocation() throws Exception { destinationPath + "/" + APK_PATH_2.getFileName()); } + @Test + public void pushFiles_sdk31_additionalPermissions() throws Exception { + String destinationPath = "/destination/path"; + when(mockDevice.getVersion()).thenReturn(new AndroidVersion(31)); + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + + mockAdbShellCommand(String.format("rm -rf '%s' && echo OK", destinationPath), "OK\n"); + mockAdbShellCommand( + String.format( + "mkdir -p '%1$s' && rmdir '%1$s' && mkdir -p '%1$s' && echo OK", destinationPath), + "OK\n"); + mockAdbShellCommand(String.format("chmod 775 '%s' && echo OK", destinationPath), "OK\n"); + + ddmlibDevice.push( + ImmutableList.of(APK_PATH, APK_PATH_2), + PushOptions.builder().setDestinationPath(destinationPath).build()); + + verify(mockDevice) + .pushFile( + APK_PATH.toFile().getAbsolutePath(), destinationPath + "/" + APK_PATH.getFileName()); + verify(mockDevice) + .pushFile( + APK_PATH_2.toFile().getAbsolutePath(), + destinationPath + "/" + APK_PATH_2.getFileName()); + verify(mockDevice) + .executeShellCommand( + eq(String.format("chmod 775 '%s' && echo OK", destinationPath)), + any(), + anyLong(), + any()); + } + @Test public void pushFiles_tempLocation() throws Exception { String destinationPath = "/destination/path"; Clock fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + when(mockDevice.getVersion()).thenReturn(new AndroidVersion(VersionCodes.KITKAT)); DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice, fixedClock); String tempPath = "/data/local/tmp/splits-" + fixedClock.millis(); @@ -193,6 +229,54 @@ public void pullFiles() throws Exception { verify(mockDevice).pullFile(APK_PATH.toFile().getAbsolutePath(), destinationPath.toString()); } + @Test + public void getDensity_densityIsAvailableViaDdmlib() throws Exception { + when(mockDevice.getDensity()).thenReturn(540); + mockAdbShellCommand("wm density", "Physical density: 420"); + + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + assertThat(ddmlibDevice.getDensity()).isEqualTo(540); + } + + @Test + public void getDensity_densityIsNotAvailableViaDdmlib_requestViaAdb() throws Exception { + when(mockDevice.getDensity()).thenReturn(-1); + mockAdbShellCommand("wm density", "Physical density: 420"); + + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + assertThat(ddmlibDevice.getDensity()).isEqualTo(420); + } + + @Test + public void getDensity_densityIsNotAvailableViaDdmlibAndAdb() throws Exception { + when(mockDevice.getDensity()).thenReturn(-1); + mockAdbShellCommand("wm density", "Test output"); + + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + assertThat(ddmlibDevice.getDensity()).isEqualTo(-1); + } + + @Test + public void removeRemotePath() throws Exception { + String pathToRemove = "/path/to/remove"; + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + + mockAdbShellCommand(String.format("rm -rf '%s' && echo OK", pathToRemove), "OK\n"); + ddmlibDevice.removeRemotePath( + pathToRemove, /* runAsPackageName= */ Optional.empty(), Duration.ofMillis(10)); + } + + @Test + public void removeRemotePath_runAs() throws Exception { + String packageName = "com.test"; + String pathToRemove = "/path/to/remove"; + DdmlibDevice ddmlibDevice = new DdmlibDevice(mockDevice); + + mockAdbShellCommand( + String.format("run-as '%s' rm -rf '%s' && echo OK", packageName, pathToRemove), "OK\n"); + ddmlibDevice.removeRemotePath(pathToRemove, Optional.of(packageName), Duration.ofMillis(10)); + } + private void mockAdbShellCommand(String command, String response) throws Exception { Mockito.doAnswer( invocation -> { diff --git a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java index 6fc1760e..4f1bf2cd 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -77,11 +77,14 @@ import com.google.protobuf.TextFormat; import java.util.Optional; import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; /** Tests for {@link AndroidManifest}. */ -@RunWith(JUnit4.class) +@RunWith(Theories.class) public class AndroidManifestTest { private static final String ANDROID_NAMESPACE_URI = "http://schemas.android.com/apk/res/android"; @@ -91,6 +94,9 @@ public class AndroidManifestTest { private static final Version BUNDLE_TOOL_0_3_4 = Version.of("0.3.4"); private static final Version BUNDLE_TOOL_0_3_3 = Version.of("0.3.3"); + @DataPoints("sdkCodenames") + public static final String[] ANDROID_SDK_CODENAMES = {"R", "Q", "Sv2", "Tiramisu"}; + @Test public void getApplicationDebuggable_absent() { AndroidManifest androidManifest = @@ -154,19 +160,11 @@ public void getMinSdkVersion_negative() { } @Test - public void getMinSdkVersion_asString() { + @Theory + public void getMinSdkVersion_asString(@FromDataPoints("sdkCodenames") String codename) { AndroidManifest androidManifest = - AndroidManifest.create(androidManifest("com.test.app", withMinSdkVersion("Q"))); + AndroidManifest.create(androidManifest("com.test.app", withMinSdkVersion(codename))); assertThat(androidManifest.getMinSdkVersion()).hasValue(DEVELOPMENT_SDK_VERSION); - - AndroidManifest androidManifest2 = - AndroidManifest.create(androidManifest("com.test.app", withMinSdkVersion("R"))); - assertThat(androidManifest2.getMinSdkVersion()).hasValue(DEVELOPMENT_SDK_VERSION); - - // Lowercase disallowed. - AndroidManifest androidManifest3 = - AndroidManifest.create(androidManifest("com.test.app", withMinSdkVersion("r"))); - assertThrows(UnexpectedAttributeTypeException.class, () -> androidManifest3.getMinSdkVersion()); } @Test @@ -1110,8 +1108,7 @@ public void getDeliveryType_legacy_onDemandFalse() throws Exception { public void getDeliveryType_onDemandElement_only() throws Exception { AndroidManifest manifest = AndroidManifest.create(androidManifest("com.test.app", withOnDemandDelivery())); - assertThat(manifest.getModuleDeliveryType()) - .isEqualTo(ModuleDeliveryType.NO_INITIAL_INSTALL); + assertThat(manifest.getModuleDeliveryType()).isEqualTo(ModuleDeliveryType.NO_INITIAL_INSTALL); } @Test @@ -1151,7 +1148,6 @@ public void getInstantDeliveryType_installTimeElement() { AndroidManifest manifest = AndroidManifest.create( androidManifestForAssetModule("com.test.app", withInstantInstallTimeDelivery())); - assertThat(manifest.getInstantModuleDeliveryType()) - .isEqualTo(ALWAYS_INITIAL_INSTALL); + assertThat(manifest.getInstantModuleDeliveryType()).isEqualTo(ALWAYS_INITIAL_INSTALL); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ResourceInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ResourceInjectorTest.java index 080870fe..8f1b840c 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ResourceInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ResourceInjectorTest.java @@ -65,9 +65,7 @@ public void missingResourceTable_resourceTableCreated() { .setTypeId(TypeId.newBuilder().setId(1)) .setName("xml") .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0))))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0))))) .build(); assertThat(resourceInjector.build()).isEqualTo(expected); @@ -97,9 +95,7 @@ public void missingXmlType_typeCreated() { .setTypeId(TypeId.newBuilder().setId(0x01)) .setName("xml") .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0x0000))))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0x0000))))) .build(); assertThat(resourceInjector.build()).isEqualTo(expected); @@ -119,16 +115,13 @@ public void multipleTypes_entryCreatedInMatchedClass() { .setTypeId(TypeId.newBuilder().setId(0x01)) .setName("drawable") .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0x0000)))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0x0000)))) .addType( Type.newBuilder() .setTypeId(TypeId.newBuilder().setId(0x02)) .setName("xml") .addEntry( - Entry.getDefaultInstance() - .toBuilder() + Entry.newBuilder() .setEntryId(EntryId.newBuilder().setId(0x0000))))); ResourceInjector resourceInjector = new ResourceInjector(resourceTable, PACKAGE_NAME); ResourceId resourceId = @@ -145,21 +138,15 @@ public void multipleTypes_entryCreatedInMatchedClass() { .setTypeId(TypeId.newBuilder().setId(0x01)) .setName("drawable") .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0x0000)))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0x0000)))) .addType( Type.newBuilder() .setTypeId(TypeId.newBuilder().setId(0x02)) .setName("xml") .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0x0000))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0x0000))) .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0x0001))))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0x0001))))) .build(); assertThat(resourceInjector.build()).isEqualTo(expected); @@ -179,16 +166,13 @@ public void noFreeEntryId_throws() { .setTypeId(TypeId.newBuilder().setId(0x01)) .setName("drawable") .addEntry( - Entry.getDefaultInstance() - .toBuilder() - .setEntryId(EntryId.newBuilder().setId(0x0000)))) + Entry.newBuilder().setEntryId(EntryId.newBuilder().setId(0x0000)))) .addType( Type.newBuilder() .setTypeId(TypeId.newBuilder().setId(0x02)) .setName("layout") .addEntry( - Entry.getDefaultInstance() - .toBuilder() + Entry.newBuilder() .setEntryId(EntryId.newBuilder().setId(0xffff))))); ResourceInjector resourceInjector = new ResourceInjector(resourceTable, PACKAGE_NAME); assertThrows( diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java index 3c0139c5..bde8e9ca 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeDevice.java @@ -42,6 +42,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -62,6 +63,7 @@ public class FakeDevice extends Device { private final Map commandInjections = new HashMap<>(); private Optional> installApksSideEffect = Optional.empty(); private Optional> pushSideEffect = Optional.empty(); + private Optional removeRemotePathSideEffect = Optional.empty(); private static final Joiner COMMA_JOINER = Joiner.on(','); private static final Joiner DASH_JOINER = Joiner.on('-'); private static final Joiner LINE_JOINER = Joiner.on(System.getProperty("line.separator")); @@ -258,7 +260,11 @@ public Path syncPackageToDevice(Path localFilePath) { } @Override - public void removeRemotePackage(Path remoteFilePath) {} + public void removeRemotePath( + String remoteFilePath, Optional runAsPackageName, Duration timeout) { + removeRemotePathSideEffect.ifPresent( + val -> val.apply(remoteFilePath, runAsPackageName, timeout)); + } @Override public void pull(ImmutableList files) { @@ -281,6 +287,10 @@ public void setPushSideEffect(SideEffect sideEffect) { pushSideEffect = Optional.of(sideEffect); } + public void setRemoveRemotePathSideEffect(RemoveRemotePathSideEffect sideEffect) { + removeRemotePathSideEffect = Optional.of(sideEffect); + } + public void clearInstallApksSideEffect() { installApksSideEffect = Optional.empty(); } @@ -297,6 +307,11 @@ String onExecute() IOException; } + /** Remove remote path side effect. */ + public interface RemoveRemotePathSideEffect { + void apply(String remotePath, Optional runAs, Duration timeout); + } + /** Side effect. */ public interface SideEffect { void apply(ImmutableList apks, T options); diff --git a/src/test/java/com/android/tools/build/bundletool/validation/EntryClashValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/EntryClashValidatorTest.java index 2ac0f0df..d1f7243e 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/EntryClashValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/EntryClashValidatorTest.java @@ -90,7 +90,7 @@ public void differentManifests_ok() throws Exception { String filePath = "manifest/AndroidManifest.xml"; byte[] fileContentA = XmlNode.getDefaultInstance().toByteArray(); byte[] fileContentB = - XmlNode.newBuilder().setElement(XmlElement.newBuilder()).build().toByteArray(); + XmlNode.newBuilder().setElement(XmlElement.getDefaultInstance()).build().toByteArray(); assertThat(fileContentA).isNotEqualTo(fileContentB); BundleModule moduleA = new BundleModuleBuilder("a").addFile(filePath, fileContentA).build(); BundleModule moduleB = new BundleModuleBuilder("b").addFile(filePath, fileContentB).build();