Add input and output consumers for android resources
This is in preperation of supporting resource shrinking in R8.
Currently we do a simple pass through of the resources.
Current API is marked as experimental in the api descriptions and compatability tests have not been added since we are not 100%
sure on the final api yet
Bug: 287399385
Change-Id: I8c45ebf0805a8ea1d3387f90106b9847dffba270
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceConsumer.java b/src/main/java/com/android/tools/r8/AndroidResourceConsumer.java
new file mode 100644
index 0000000..634611f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceConsumer.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+@Keep
+public interface AndroidResourceConsumer {
+
+ void accept(AndroidResourceOutput androidResource, DiagnosticsHandler diagnosticsHandler);
+
+ default void finished(DiagnosticsHandler handler) {}
+}
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceInput.java b/src/main/java/com/android/tools/r8/AndroidResourceInput.java
new file mode 100644
index 0000000..00f61c7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceInput.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import java.io.InputStream;
+
+/**
+ * Base interface for android resources (resource table, manifest, res folder files)
+ *
+ * <p>Android resources are provided to R8 to allow for combined code and resource shrinking. This
+ * allows us to reason about resources, and transitively other resources and code throughout the
+ * entire compilation pipeline.
+ */
+@Keep
+public interface AndroidResourceInput extends Resource {
+ enum Kind {
+ // The AndroidManifest.xml file.
+ MANIFEST,
+ // The resource table, in either binary or proto format.
+ RESOURCE_TABLE,
+ // An xml file within the res folder.
+ XML_FILE,
+ // Any other binary file withing the res folder.
+ RES_FOLDER_FILE
+ }
+
+ // The path, within the app, of the resource.
+ ResourcePath getPath();
+
+ Kind getKind();
+
+ InputStream getByteStream() throws ResourceException;
+}
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceOutput.java b/src/main/java/com/android/tools/r8/AndroidResourceOutput.java
new file mode 100644
index 0000000..6b204b5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceOutput.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+/**
+ * Base interface for android resources (resource table, manifest, res folder files)
+ *
+ * <p>Android resources are provided to R8 to allow for combined code and resource shrinking. This
+ * allows us to reason about resources, and transitively other resources and code throughout the
+ * entire compilation pipeline.
+ */
+@Keep
+public interface AndroidResourceOutput extends Resource {
+ // The path, within the app, of the resource.
+ ResourcePath getPath();
+
+ ByteDataView getByteDataView();
+}
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceProvider.java b/src/main/java/com/android/tools/r8/AndroidResourceProvider.java
new file mode 100644
index 0000000..cb44ff0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/AndroidResourceProvider.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import java.util.Collection;
+
+/** Android resource provider. */
+@Keep
+public interface AndroidResourceProvider {
+
+ // Provide all android resources
+ Collection<AndroidResourceInput> getAndroidResources() throws ResourceException;
+
+ default void finished(DiagnosticsHandler handler) {}
+}
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
new file mode 100644
index 0000000..c601e80
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.utils.ArchiveBuilder;
+import com.android.tools.r8.utils.OutputBuilder;
+import java.nio.file.Path;
+
+public class ArchiveProtoAndroidResourceConsumer implements AndroidResourceConsumer {
+ private final OutputBuilder outputBuilder;
+
+ public ArchiveProtoAndroidResourceConsumer(Path outputPath) {
+ this.outputBuilder = new ArchiveBuilder(outputPath);
+ this.outputBuilder.open();
+ }
+
+ @Override
+ public void accept(AndroidResourceOutput androidResource, DiagnosticsHandler diagnosticsHandler) {
+ outputBuilder.addFile(
+ androidResource.getPath().location(),
+ androidResource.getByteDataView(),
+ diagnosticsHandler);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ outputBuilder.close(handler);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
new file mode 100644
index 0000000..ecf9970
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.AndroidResourceInput.Kind;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Provides android resources from an archive.
+ *
+ * <p>The descriptor index is built eagerly upon creating the provider and subsequent requests for
+ * resources in the descriptor set will then force the read of zip entry contents.
+ */
+@Keep
+public class ArchiveProtoAndroidResourceProvider implements AndroidResourceProvider {
+ private final Path archive;
+ private final Origin origin;
+
+ private static final String MANIFEST_NAME = "AndroidManifest.xml";
+ private static final String RESOURCE_TABLE = "resources.pb";
+ private static final String RES_FOLDER = "res/";
+ private static final String XML_SUFFIX = ".xml";
+
+ /**
+ * Creates an android resource provider from an archive.
+ *
+ * @param archive Zip archive to provide resources from.
+ * @param origin Origin of the archive.
+ */
+ public ArchiveProtoAndroidResourceProvider(Path archive, Origin origin) {
+ this.archive = archive;
+ this.origin = origin;
+ }
+
+ @Override
+ public Collection<AndroidResourceInput> getAndroidResources() throws ResourceException {
+ try (ZipFile zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8)) {
+ List<AndroidResourceInput> resources = new ArrayList<>();
+ final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ ByteAndroidResourceInput resource =
+ new ByteAndroidResourceInput(
+ name,
+ getKindFromName(name),
+ ByteStreams.toByteArray(zipFile.getInputStream(entry)),
+ new ArchiveEntryOrigin(name, origin));
+ resources.add(resource);
+ }
+ return resources;
+ } catch (IOException e) {
+ throw new ResourceException(origin, e);
+ }
+ }
+
+ private Kind getKindFromName(String name) {
+ if (name.equals(MANIFEST_NAME)) {
+ return Kind.MANIFEST;
+ }
+ if (name.equals(RESOURCE_TABLE)) {
+ return Kind.RESOURCE_TABLE;
+ }
+ if (!name.startsWith(RES_FOLDER)) {
+ throw new CompilationError("Unexpected non resource entry " + name, origin);
+ }
+ if (name.endsWith(XML_SUFFIX)) {
+ return Kind.XML_FILE;
+ }
+ return Kind.RES_FOLDER_FILE;
+ }
+
+ private static class ByteAndroidResourceInput implements AndroidResourceInput {
+
+ private final String name;
+ private final Kind kind;
+ private final byte[] bytes;
+ private final Origin origin;
+
+ public ByteAndroidResourceInput(String name, Kind kind, byte[] bytes, Origin origin) {
+ this.name = name;
+ this.kind = kind;
+ this.bytes = bytes;
+ this.origin = origin;
+ }
+
+ @Override
+ public ResourcePath getPath() {
+ return () -> name;
+ }
+
+ @Override
+ public Kind getKind() {
+ return kind;
+ }
+
+ @Override
+ public InputStream getByteStream() throws ResourceException {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public Origin getOrigin() {
+ return origin;
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 56d0a8c..0ce7a4e 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -72,6 +72,7 @@
import com.android.tools.r8.optimize.proto.ProtoNormalizer;
import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemover;
import com.android.tools.r8.origin.CommandLineOrigin;
+import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
import com.android.tools.r8.repackaging.Repackaging;
@@ -102,6 +103,7 @@
import com.android.tools.r8.synthesis.SyntheticFinalization;
import com.android.tools.r8.synthesis.SyntheticItems;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.ExceptionUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.SelfRetraceTest;
@@ -804,6 +806,12 @@
new DesugaredLibraryKeepRuleGenerator(appView).runIfNecessary(timing);
+ if (options.androidResourceProvider != null && options.androidResourceConsumer != null) {
+ // Currently this is simply a pass through of all resources.
+ writeAndroidResources(
+ options.androidResourceProvider, options.androidResourceConsumer, appView.reporter());
+ }
+
// Generate the resulting application resources.
writeApplication(appView, inputApp, executorService);
@@ -822,6 +830,44 @@
}
}
+ private void writeAndroidResources(
+ AndroidResourceProvider androidResourceProvider,
+ AndroidResourceConsumer androidResourceConsumer,
+ DiagnosticsHandler diagnosticsHandler) {
+ try {
+ for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
+ androidResourceConsumer.accept(
+ new AndroidResourceOutput() {
+ @Override
+ public ResourcePath getPath() {
+ return androidResource.getPath();
+ }
+
+ @Override
+ public ByteDataView getByteDataView() {
+ try {
+ return ByteDataView.of(ByteStreams.toByteArray(androidResource.getByteStream()));
+ } catch (IOException | ResourceException e) {
+ diagnosticsHandler.error(new ExceptionDiagnostic(e, androidResource.getOrigin()));
+ }
+ return null;
+ }
+
+ @Override
+ public Origin getOrigin() {
+ return androidResource.getOrigin();
+ }
+ },
+ diagnosticsHandler);
+ }
+ } catch (ResourceException e) {
+ throw new RuntimeException("Cannot write android resources", e);
+ } finally {
+ androidResourceConsumer.finished(diagnosticsHandler);
+ androidResourceProvider.finished(diagnosticsHandler);
+ }
+ }
+
private static boolean allReferencesAssignedApiLevel(
AppView<? extends AppInfoWithClassHierarchy> appView) {
if (!appView.options().apiModelingOptions().isCheckAllApiReferencesAreSet()
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 7ec72c1..4e89913 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -135,6 +135,8 @@
private boolean enableMissingLibraryApiModeling = false;
private boolean enableExperimentalKeepAnnotations = false;
private SemanticVersion fakeCompilerVersion = null;
+ private AndroidResourceProvider androidResourceProvider = null;
+ private AndroidResourceConsumer androidResourceConsumer = null;
private final ProguardConfigurationParserOptions.Builder parserOptionsBuilder =
ProguardConfigurationParserOptions.builder().readEnvironment();
@@ -506,6 +508,28 @@
return super.addStartupProfileProviders(startupProfileProviders);
}
+ /**
+ * Exprimental API for supporting android resource shrinking.
+ *
+ * <p>Add an android resource provider, providing the resource table, manifest and res table
+ * entries.
+ */
+ public Builder setAndroidResourceProvider(AndroidResourceProvider provider) {
+ this.androidResourceProvider = provider;
+ return this;
+ }
+
+ /**
+ * Exprimental API for supporting android resource shrinking.
+ *
+ * <p>Add an android resource consumer, consuming the resource table, manifest and res table
+ * entries.
+ */
+ public Builder setAndroidResourceConsumer(AndroidResourceConsumer consumer) {
+ this.androidResourceConsumer = consumer;
+ return this;
+ }
+
@Override
void validate() {
if (isPrintHelp()) {
@@ -659,7 +683,9 @@
getArtProfilesForRewriting(),
getStartupProfileProviders(),
getClassConflictResolver(),
- getCancelCompilationChecker());
+ getCancelCompilationChecker(),
+ androidResourceProvider,
+ androidResourceConsumer);
if (inputDependencyGraphConsumer != null) {
inputDependencyGraphConsumer.finished();
@@ -847,6 +873,8 @@
private final FeatureSplitConfiguration featureSplitConfiguration;
private final String synthesizedClassPrefix;
private final boolean enableMissingLibraryApiModeling;
+ private final AndroidResourceProvider androidResourceProvider;
+ private final AndroidResourceConsumer androidResourceConsumer;
/** Get a new {@link R8Command.Builder}. */
public static Builder builder() {
@@ -941,7 +969,9 @@
List<ArtProfileForRewriting> artProfilesForRewriting,
List<StartupProfileProvider> startupProfileProviders,
ClassConflictResolver classConflictResolver,
- CancelCompilationChecker cancelCompilationChecker) {
+ CancelCompilationChecker cancelCompilationChecker,
+ AndroidResourceProvider androidResourceProvider,
+ AndroidResourceConsumer androidResourceConsumer) {
super(
inputApp,
mode,
@@ -986,6 +1016,8 @@
this.featureSplitConfiguration = featureSplitConfiguration;
this.synthesizedClassPrefix = synthesizedClassPrefix;
this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
+ this.androidResourceProvider = androidResourceProvider;
+ this.androidResourceConsumer = androidResourceConsumer;
}
private R8Command(boolean printHelp, boolean printVersion) {
@@ -1010,6 +1042,8 @@
featureSplitConfiguration = null;
synthesizedClassPrefix = null;
enableMissingLibraryApiModeling = false;
+ androidResourceProvider = null;
+ androidResourceConsumer = null;
}
public DexItemFactory getDexItemFactory() {
@@ -1182,6 +1216,9 @@
internal.cancelCompilationChecker = getCancelCompilationChecker();
+ internal.androidResourceProvider = androidResourceProvider;
+ internal.androidResourceConsumer = androidResourceConsumer;
+
if (!DETERMINISTIC_DEBUGGING) {
assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
internal.threadCount = getThreadCount();
diff --git a/src/main/java/com/android/tools/r8/ResourcePath.java b/src/main/java/com/android/tools/r8/ResourcePath.java
new file mode 100644
index 0000000..eab4d7e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ResourcePath.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+@Keep
+public interface ResourcePath {
+
+ // The location within the apk, bundle or resource directory, e.g., res/xml/foo.xml
+ String location();
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9afb2e9..09f5a8c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -6,6 +6,8 @@
import static com.android.tools.r8.utils.AndroidApiLevel.B;
import static com.android.tools.r8.utils.SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault;
+import com.android.tools.r8.AndroidResourceConsumer;
+import com.android.tools.r8.AndroidResourceProvider;
import com.android.tools.r8.CancelCompilationChecker;
import com.android.tools.r8.ClassFileConsumer;
import com.android.tools.r8.CompilationMode;
@@ -173,6 +175,8 @@
// The state can only ever transition from false to true.
private final AtomicBoolean cancelled = new AtomicBoolean(false);
public CancelCompilationChecker cancelCompilationChecker = null;
+ public AndroidResourceProvider androidResourceProvider = null;
+ public AndroidResourceConsumer androidResourceConsumer = null;
public boolean checkIfCancelled() {
if (cancelCompilationChecker == null) {
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
new file mode 100644
index 0000000..b58587b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import java.nio.charset.StandardCharsets;
+
+public class AndroidResourceTestingUtils {
+
+ // Taken from default empty android studio activity template
+ public static String TEST_MANIFEST =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " xmlns:tools=\"http://schemas.android.com/tools\">\n"
+ + "\n"
+ + " <application\n"
+ + " android:allowBackup=\"true\"\n"
+ + " android:dataExtractionRules=\"@xml/data_extraction_rules\"\n"
+ + " android:fullBackupContent=\"@xml/backup_rules\"\n"
+ + " android:icon=\"@mipmap/ic_launcher\"\n"
+ + " android:label=\"@string/app_name\"\n"
+ + " android:roundIcon=\"@mipmap/ic_launcher_round\"\n"
+ + " android:supportsRtl=\"true\"\n"
+ + " android:theme=\"@style/Theme.MyApplication\"\n"
+ + " tools:targetApi=\"31\">\n"
+ + " <activity\n"
+ + " android:name=\".MainActivity\"\n"
+ + " android:exported=\"true\"\n"
+ + " android:label=\"@string/app_name\"\n"
+ + " android:theme=\"@style/Theme.MyApplication.NoActionBar\">\n"
+ + " <intent-filter>\n"
+ + " <action android:name=\"android.intent.action.MAIN\" />\n"
+ + "\n"
+ + " <category android:name=\"android.intent.category.LAUNCHER\" />\n"
+ + " </intent-filter>\n"
+ + "\n"
+ + " <meta-data\n"
+ + " android:name=\"android.app.lib_name\"\n"
+ + " android:value=\"\" />\n"
+ + " </activity>\n"
+ + " </application>\n"
+ + "\n"
+ + "</manifest>";
+
+ // TODO(287399385): Add testing utils for generating/consuming resource tables.
+ public static byte[] TEST_RESOURCE_TABLE = "RESOURCE_TABLE".getBytes(StandardCharsets.UTF_8);
+
+ // The below byte arrays are lifted from the resource shrinkers DummyContent
+
+ // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
+ public static final byte[] TINY_PNG =
+ new byte[] {
+ (byte) -119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
+ (byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,
+ (byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 1,
+ (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 58,
+ (byte) 126, (byte) -101, (byte) 85, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 10, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,
+ (byte) -38, (byte) 99, (byte) 96, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 2, (byte) 0, (byte) 1, (byte) -27, (byte) 39, (byte) -34,
+ (byte) -4, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 73,
+ (byte) 69, (byte) 78, (byte) 68, (byte) -82, (byte) 66, (byte) 96,
+ (byte) -126
+ };
+
+ // The XML document <x/> as a proto packed with AAPT2
+ public static final byte[] TINY_PROTO_XML =
+ new byte[] {0xa, 0x3, 0x1a, 0x1, 0x78, 0x1a, 0x2, 0x8, 0x1};
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
new file mode 100644
index 0000000..f0bb82a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourcesPassthroughTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
+import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AndroidResourcesPassthroughTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ String manifestPath = "AndroidManifest.xml";
+ String resourcePath = "resources.pb";
+ String pngPath = "res/drawable/foo.png";
+ String xmlPath = "res/xml/bar.xml";
+ Path resources =
+ ZipBuilder.builder(temp.newFile("resources.zip").toPath())
+ .addText(manifestPath, AndroidResourceTestingUtils.TEST_MANIFEST)
+ .addBytes(resourcePath, AndroidResourceTestingUtils.TEST_RESOURCE_TABLE)
+ .addBytes(pngPath, AndroidResourceTestingUtils.TINY_PNG)
+ .addBytes(xmlPath, AndroidResourceTestingUtils.TINY_PROTO_XML)
+ .build();
+ Path output = temp.newFile("resources_out.zip").toPath();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .addOptionsModification(
+ o -> {
+ o.androidResourceProvider =
+ new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources));
+ o.androidResourceConsumer = new ArchiveProtoAndroidResourceConsumer(output);
+ })
+ .addKeepMainRule(FooBar.class)
+ .run(parameters.getRuntime(), FooBar.class)
+ .assertSuccessWithOutputLines("Hello World");
+ assertTrue(
+ Arrays.equals(
+ ZipUtils.readSingleEntry(output, manifestPath),
+ AndroidResourceTestingUtils.TEST_MANIFEST.getBytes(StandardCharsets.UTF_8)));
+ assertTrue(
+ Arrays.equals(
+ ZipUtils.readSingleEntry(output, resourcePath),
+ AndroidResourceTestingUtils.TEST_RESOURCE_TABLE));
+ assertTrue(
+ Arrays.equals(
+ ZipUtils.readSingleEntry(output, pngPath), AndroidResourceTestingUtils.TINY_PNG));
+ assertTrue(
+ Arrays.equals(
+ ZipUtils.readSingleEntry(output, xmlPath), AndroidResourceTestingUtils.TINY_PROTO_XML));
+ }
+
+ public static class FooBar {
+
+ public static void main(String[] args) {
+ System.out.println("Hello World");
+ }
+ }
+}