Merge "Add resource shrinker API to R8"
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinker.java b/src/main/java/com/android/tools/r8/ResourceShrinker.java
new file mode 100644
index 0000000..1df78be
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ResourceShrinker.java
@@ -0,0 +1,463 @@
+// Copyright (c) 2017, 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.code.Const;
+import com.android.tools.r8.code.Const16;
+import com.android.tools.r8.code.Const4;
+import com.android.tools.r8.code.ConstHigh16;
+import com.android.tools.r8.code.ConstString;
+import com.android.tools.r8.code.ConstStringJumbo;
+import com.android.tools.r8.code.ConstWide16;
+import com.android.tools.r8.code.ConstWide32;
+import com.android.tools.r8.code.FillArrayData;
+import com.android.tools.r8.code.FillArrayDataPayload;
+import com.android.tools.r8.code.Format35c;
+import com.android.tools.r8.code.Format3rc;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.InvokeDirect;
+import com.android.tools.r8.code.InvokeDirectRange;
+import com.android.tools.r8.code.InvokeInterface;
+import com.android.tools.r8.code.InvokeInterfaceRange;
+import com.android.tools.r8.code.InvokeStatic;
+import com.android.tools.r8.code.InvokeStaticRange;
+import com.android.tools.r8.code.InvokeSuper;
+import com.android.tools.r8.code.InvokeSuperRange;
+import com.android.tools.r8.code.InvokeVirtual;
+import com.android.tools.r8.code.InvokeVirtualRange;
+import com.android.tools.r8.code.NewArray;
+import com.android.tools.r8.code.Sget;
+import com.android.tools.r8.code.SgetBoolean;
+import com.android.tools.r8.code.SgetByte;
+import com.android.tools.r8.code.SgetChar;
+import com.android.tools.r8.code.SgetObject;
+import com.android.tools.r8.code.SgetShort;
+import com.android.tools.r8.code.SgetWide;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.ir.code.SingleConstant;
+import com.android.tools.r8.ir.code.WideConstant;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Stream;
+
+/**
+ * This class is deprecated and should not be used. It is a temporary solution to use R8 to analyze
+ * dex files and compute resource shrinker related bits.
+ *
+ * <p>Users or this API should implement {@link ReferenceChecker} interface, and through callbacks
+ * in that interface, they will be notified when element relevant for resource shrinking is found.
+ *
+ * <p>This class extracts all integer constants and string constants, which might refer to resource.
+ * More specifically, we look for the following while analyzing dex:
+ * <ul>
+ * <li>const instructions that might load integers or strings
+ * <li>static fields that have an initial value. This initial value might be integer, string,
+ * or array of integers.
+ * <li>integer array payloads. Only payloads referenced in fill-array-data instructions will be
+ * processed. More specifically, if a payload is referenced in fill-array-data, and we are able
+ * to determine that array is not array of integers, payload will be ignored. Otherwise, it will
+ * be processed once fill-array-data-payload instruction is encountered.
+ * <li>all annotations (class, field, method) that contain annotation element whose value is
+ * integer, string or array of integers are processed.
+ * </ul>
+ *
+ * <p>Please note that switch payloads are not analyzed. Although they might contain integer
+ * constants, ones referring to resource ids would have to be loaded in the code analyzed in list
+ * above.
+ *
+ * <p>Usage of this feature is intentionally not supported from the command line.
+ */
+@Deprecated
+final public class ResourceShrinker {
+
+ final static class Command extends BaseCommand {
+
+ Command(AndroidApp app) {
+ super(app);
+ }
+
+ @Override
+ InternalOptions getInternalOptions() {
+ return new InternalOptions();
+ }
+ }
+
+ final public static class Builder extends BaseCommand.Builder<Command, Builder> {
+
+ @Override
+ Builder self() {
+ return this;
+ }
+
+ @Override
+ Command makeCommand() {
+ return new Command(getAppBuilder().build());
+ }
+ }
+
+ /**
+ * Classes that would like to process data relevant to resource shrinking should implement this
+ * interface.
+ */
+ interface ReferenceChecker {
+
+ /**
+ * Returns if the class with specified internal name should be processed. Typically,
+ * resource type classes like R$drawable, R$styleable etc. should be skipped.
+ */
+ boolean shouldProcess(String internalName);
+
+ void referencedInt(int value);
+
+ void referencedString(String value);
+
+ void referencedStaticField(String internalName, String fieldName);
+
+ void referencedMethod(String internalName, String methodName, String methodDescriptor);
+ }
+
+ private static final class DexClassUsageVisitor {
+
+ private final DexProgramClass classDef;
+ private final ReferenceChecker callback;
+
+ DexClassUsageVisitor(DexProgramClass classDef, ReferenceChecker callback) {
+ this.classDef = classDef;
+ this.callback = callback;
+ }
+
+ public void visit() {
+ if (!callback.shouldProcess(classDef.type.getInternalName())) {
+ return;
+ }
+
+ for (DexEncodedField field : classDef.staticFields()) {
+ if (field.staticValue != null) {
+ processFieldValue(field.staticValue);
+ }
+ }
+
+ for (DexEncodedMethod method : classDef.allMethodsSorted()) {
+ processMethod(method);
+ }
+
+ if (classDef.hasAnnotations()) {
+ processAnnotations(classDef);
+ }
+ }
+
+ private void processFieldValue(DexValue value) {
+ if (value instanceof DexValue.DexValueString) {
+ callback.referencedString(((DexValue.DexValueString) value).value.toString());
+ } else if (value instanceof DexValue.DexValueInt) {
+ int constantValue = ((DexValue.DexValueInt) value).getValue();
+ callback.referencedInt(constantValue);
+ } else if (value instanceof DexValue.DexValueArray) {
+ DexValue.DexValueArray arrayEncodedValue = (DexValue.DexValueArray) value;
+ for (DexValue encodedValue : arrayEncodedValue.getValues()) {
+ if (encodedValue instanceof DexValue.DexValueInt) {
+ int constantValue = ((DexValue.DexValueInt) encodedValue).getValue();
+ callback.referencedInt(constantValue);
+ }
+ }
+ }
+ }
+
+ private void processMethod(DexEncodedMethod method) {
+ Code implementation = method.getCode();
+ if (implementation != null) {
+
+ // Tracks the offsets of integer array payloads.
+ final Set<Integer> methodIntArrayPayloadOffsets = Sets.newHashSet();
+ // First we collect payloads, and then we process them because payload can be before the
+ // fill-array-data instruction referencing it.
+ final List<FillArrayDataPayload> payloads = Lists.newArrayList();
+
+ Instruction[] instructions = implementation.asDexCode().instructions;
+ int current = 0;
+ while (current < instructions.length) {
+ Instruction instruction = instructions[current];
+ if (isIntConstInstruction(instruction)) {
+ processIntConstInstruction(instruction);
+ } else if (isStringConstInstruction(instruction)) {
+ processStringConstantInstruction(instruction);
+ } else if (isGetStatic(instruction)) {
+ processGetStatic(instruction);
+ } else if (isInvokeInstruction(instruction)) {
+ processInvokeInstruction(instruction);
+ } else if (isInvokeRangeInstruction(instruction)) {
+ processInvokeRangeInstruction(instruction);
+ } else if (instruction instanceof FillArrayData) {
+ processFillArray(instructions, current, methodIntArrayPayloadOffsets);
+ } else if (instruction instanceof FillArrayDataPayload) {
+ payloads.add((FillArrayDataPayload) instruction);
+ }
+ current++;
+ }
+
+ for (FillArrayDataPayload payload : payloads) {
+ if (isIntArrayPayload(payload, methodIntArrayPayloadOffsets)) {
+ processIntArrayPayload(payload);
+ }
+ }
+ }
+ }
+
+ private void processAnnotations(DexProgramClass classDef) {
+ Stream<DexAnnotation> instanceFieldAnnotations =
+ Arrays.stream(classDef.instanceFields())
+ .filter(DexEncodedField::hasAnnotation)
+ .flatMap(f -> Arrays.stream(f.annotations.annotations));
+ Stream<DexAnnotation> staticFieldAnnotations =
+ Arrays.stream(classDef.staticFields())
+ .filter(DexEncodedField::hasAnnotation)
+ .flatMap(f -> Arrays.stream(f.annotations.annotations));
+ Stream<DexAnnotation> virtualMethodAnnotations =
+ Arrays.stream(classDef.virtualMethods())
+ .filter(DexEncodedMethod::hasAnnotation)
+ .flatMap(m -> Arrays.stream(m.annotations.annotations));
+ Stream<DexAnnotation> directMethodAnnotations =
+ Arrays.stream(classDef.directMethods())
+ .filter(DexEncodedMethod::hasAnnotation)
+ .flatMap(m -> Arrays.stream(m.annotations.annotations));
+ Stream<DexAnnotation> classAnnotations = Arrays.stream(classDef.annotations.annotations);
+
+ Streams.concat(
+ instanceFieldAnnotations,
+ staticFieldAnnotations,
+ virtualMethodAnnotations,
+ directMethodAnnotations,
+ classAnnotations)
+ .forEach(annotation -> {
+ for (DexAnnotationElement element : annotation.annotation.elements) {
+ DexValue value = element.value;
+ processAnnotationValue(value);
+ }
+ });
+ }
+
+ private void processIntArrayPayload(Instruction instruction) {
+ FillArrayDataPayload payload = (FillArrayDataPayload) instruction;
+
+ for (int i = 0; i < payload.data.length / 2; i++) {
+ int intValue = payload.data[2 * i + 1] << 16 | payload.data[2 * i];
+ callback.referencedInt(intValue);
+ }
+ }
+
+ private boolean isIntArrayPayload(
+ Instruction instruction, Set<Integer> methodIntArrayPayloadOffsets) {
+ if (!(instruction instanceof FillArrayDataPayload)) {
+ return false;
+ }
+
+ FillArrayDataPayload payload = (FillArrayDataPayload) instruction;
+ return methodIntArrayPayloadOffsets.contains(payload.getOffset());
+ }
+
+ private void processFillArray(
+ Instruction[] instructions, int current, Set<Integer> methodIntArrayPayloadOffsets) {
+ FillArrayData fillArrayData = (FillArrayData) instructions[current];
+ if (current > 0 && instructions[current - 1] instanceof NewArray) {
+ NewArray newArray = (NewArray) instructions[current - 1];
+ if (!Objects.equals(newArray.getType().descriptor.toString(), "[I")) {
+ return;
+ }
+ // Typically, new-array is right before fill-array-data. If not, assume referenced array is
+ // of integers. This can be improved later, but for now we make sure no ints are missed.
+ }
+
+ methodIntArrayPayloadOffsets.add(fillArrayData.getPayloadOffset() + fillArrayData.offset);
+ }
+
+ private void processAnnotationValue(DexValue value) {
+ if (value instanceof DexValue.DexValueInt) {
+ DexValue.DexValueInt dexValueInt = (DexValue.DexValueInt) value;
+ callback.referencedInt(dexValueInt.value);
+ } else if (value instanceof DexValue.DexValueString) {
+ DexValue.DexValueString dexValueString = (DexValue.DexValueString) value;
+ callback.referencedString(dexValueString.value.toString());
+ } else if (value instanceof DexValue.DexValueArray) {
+ DexValue.DexValueArray dexValueArray = (DexValue.DexValueArray) value;
+ for (DexValue dexValue : dexValueArray.getValues()) {
+ processAnnotationValue(dexValue);
+ }
+ } else if (value instanceof DexValue.DexValueAnnotation) {
+ DexValue.DexValueAnnotation dexValueAnnotation = (DexValue.DexValueAnnotation) value;
+ for (DexAnnotationElement element : dexValueAnnotation.value.elements) {
+ processAnnotationValue(element.value);
+ }
+ }
+ }
+
+ private boolean isIntConstInstruction(Instruction instruction) {
+ int opcode = instruction.getOpcode();
+ return opcode == Const4.OPCODE
+ || opcode == Const16.OPCODE
+ || opcode == Const.OPCODE
+ || opcode == ConstWide32.OPCODE
+ || opcode == ConstHigh16.OPCODE
+ || opcode == ConstWide16.OPCODE;
+ }
+
+ private void processIntConstInstruction(Instruction instruction) {
+ assert isIntConstInstruction(instruction);
+
+ int constantValue;
+ if (instruction instanceof SingleConstant) {
+ SingleConstant singleConstant = (SingleConstant) instruction;
+ constantValue = singleConstant.decodedValue();
+ } else if (instruction instanceof WideConstant) {
+ WideConstant wideConstant = (WideConstant) instruction;
+ if (((int) wideConstant.decodedValue()) != wideConstant.decodedValue()) {
+ // We care only about values that fit in int range.
+ return;
+ }
+ constantValue = (int) wideConstant.decodedValue();
+ } else {
+ throw new AssertionError("Not an int const instruction.");
+ }
+
+ callback.referencedInt(constantValue);
+ }
+
+ private boolean isStringConstInstruction(Instruction instruction) {
+ int opcode = instruction.getOpcode();
+ return opcode == ConstString.OPCODE || opcode == ConstStringJumbo.OPCODE;
+ }
+
+ private void processStringConstantInstruction(Instruction instruction) {
+ assert isStringConstInstruction(instruction);
+
+ String constantValue;
+ if (instruction instanceof ConstString) {
+ ConstString constString = (ConstString) instruction;
+ constantValue = constString.getString().toString();
+ } else if (instruction instanceof ConstStringJumbo) {
+ ConstStringJumbo constStringJumbo = (ConstStringJumbo) instruction;
+ constantValue = constStringJumbo.getString().toString();
+ } else {
+ throw new AssertionError("Not a string constant instruction.");
+ }
+
+ callback.referencedString(constantValue);
+ }
+
+ private boolean isGetStatic(Instruction instruction) {
+ int opcode = instruction.getOpcode();
+ return opcode == Sget.OPCODE
+ || opcode == SgetBoolean.OPCODE
+ || opcode == SgetByte.OPCODE
+ || opcode == SgetChar.OPCODE
+ || opcode == SgetObject.OPCODE
+ || opcode == SgetShort.OPCODE
+ || opcode == SgetWide.OPCODE;
+ }
+
+ private void processGetStatic(Instruction instruction) {
+ assert isGetStatic(instruction);
+
+ DexField field;
+ if (instruction instanceof Sget) {
+ Sget sget = (Sget) instruction;
+ field = sget.getField();
+ } else if (instruction instanceof SgetBoolean) {
+ SgetBoolean sgetBoolean = (SgetBoolean) instruction;
+ field = sgetBoolean.getField();
+ } else if (instruction instanceof SgetByte) {
+ SgetByte sgetByte = (SgetByte) instruction;
+ field = sgetByte.getField();
+ } else if (instruction instanceof SgetChar) {
+ SgetChar sgetChar = (SgetChar) instruction;
+ field = sgetChar.getField();
+ } else if (instruction instanceof SgetObject) {
+ SgetObject sgetObject = (SgetObject) instruction;
+ field = sgetObject.getField();
+ } else if (instruction instanceof SgetShort) {
+ SgetShort sgetShort = (SgetShort) instruction;
+ field = sgetShort.getField();
+ } else if (instruction instanceof SgetWide) {
+ SgetWide sgetWide = (SgetWide) instruction;
+ field = sgetWide.getField();
+ } else {
+ throw new AssertionError("Not a get static instruction");
+ }
+
+ callback.referencedStaticField(field.clazz.getInternalName(), field.name.toString());
+ }
+
+ private boolean isInvokeInstruction(Instruction instruction) {
+ int opcode = instruction.getOpcode();
+ return opcode == InvokeVirtual.OPCODE
+ || opcode == InvokeSuper.OPCODE
+ || opcode == InvokeDirect.OPCODE
+ || opcode == InvokeStatic.OPCODE
+ || opcode == InvokeInterface.OPCODE;
+ }
+
+ private void processInvokeInstruction(Instruction instruction) {
+ assert isInvokeInstruction(instruction);
+
+ Format35c ins35c = (Format35c) instruction;
+ DexMethod method = (DexMethod) ins35c.BBBB;
+
+ callback.referencedMethod(
+ method.holder.getInternalName(),
+ method.name.toString(),
+ method.proto.toDescriptorString());
+ }
+
+ private boolean isInvokeRangeInstruction(Instruction instruction) {
+ int opcode = instruction.getOpcode();
+ return opcode == InvokeVirtualRange.OPCODE
+ || opcode == InvokeSuperRange.OPCODE
+ || opcode == InvokeDirectRange.OPCODE
+ || opcode == InvokeStaticRange.OPCODE
+ || opcode == InvokeInterfaceRange.OPCODE;
+ }
+
+ private void processInvokeRangeInstruction(Instruction instruction) {
+ assert isInvokeRangeInstruction(instruction);
+
+ Format3rc ins3rc = (Format3rc) instruction;
+ DexMethod method = (DexMethod) ins3rc.BBBB;
+
+ callback.referencedMethod(
+ method.holder.getInternalName(),
+ method.name.toString(),
+ method.proto.toDescriptorString());
+ }
+ }
+
+ public static void run(Command command, ReferenceChecker callback)
+ throws IOException, ExecutionException {
+ AndroidApp inputApp = command.getInputApp();
+ Timing timing = new Timing("resource shrinker analyzer");
+ DexApplication dexApplication =
+ new ApplicationReader(inputApp, command.getInternalOptions(), timing).read();
+ for (DexProgramClass programClass : dexApplication.classes()) {
+ new DexClassUsageVisitor(programClass, callback).visit();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java b/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java
new file mode 100644
index 0000000..301f78f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java
@@ -0,0 +1,245 @@
+// Copyright (c) 2017, 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 static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests for resource shrinker analyzer. This is checking that dex files are processed correctly.
+ */
+public class ResourceShrinkerTest extends TestBase {
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ private static class TrackAll implements ResourceShrinker.ReferenceChecker {
+ Set<Integer> integers = Sets.newHashSet();
+ Set<String> strings = Sets.newHashSet();
+ List<List<String>> fields = Lists.newArrayList();
+ List<List<String>> methods = Lists.newArrayList();
+
+ @Override
+ public boolean shouldProcess(String internalName) {
+ return !internalName.equals(ResourceClassToSkip.class.getName());
+ }
+
+ @Override
+ public void referencedInt(int value) {
+ integers.add(value);
+ }
+
+ @Override
+ public void referencedString(String value) {
+ strings.add(value);
+ }
+
+ @Override
+ public void referencedStaticField(String internalName, String fieldName) {
+ fields.add(Lists.newArrayList(internalName, fieldName));
+ }
+
+ @Override
+ public void referencedMethod(String internalName, String methodName, String methodDescriptor) {
+ if (Objects.equals(internalName, "java/lang/Object")
+ && Objects.equals(methodName, "<init>")) {
+ return;
+ }
+ methods.add(Lists.newArrayList(internalName, methodName, methodDescriptor));
+ }
+ }
+
+ private static class EmptyClass {
+ }
+
+ @Test
+ public void testEmptyClass()
+ throws CompilationFailedException, IOException, ExecutionException, CompilationException {
+ TrackAll analysis = runAnalysis(EmptyClass.class);
+
+ assertThat(analysis.integers, is(Sets.newHashSet()));
+ assertThat(analysis.strings, is(Sets.newHashSet()));
+ assertThat(analysis.fields, is(Lists.newArrayList()));
+ assertThat(analysis.methods, is(Lists.newArrayList()));
+ }
+
+ private static class ConstInCode {
+ public void foo() {
+ int i = 10;
+ System.out.print(i);
+ System.out.print(11);
+ String s = "my_layout";
+ System.out.print("another_layout");
+ }
+ }
+
+ @Test
+ public void testConstsAndFieldAndMethods()
+ throws CompilationFailedException, IOException, ExecutionException, CompilationException {
+ TrackAll analysis = runAnalysis(ConstInCode.class);
+
+ assertThat(analysis.integers, is(Sets.newHashSet(10, 11)));
+ assertThat(analysis.strings, is(Sets.newHashSet("my_layout", "another_layout")));
+
+ assertEquals(3, analysis.fields.size());
+ assertThat(analysis.fields.get(0), is(Lists.newArrayList("java/lang/System", "out")));
+ assertThat(analysis.fields.get(1), is(Lists.newArrayList("java/lang/System", "out")));
+ assertThat(analysis.fields.get(2), is(Lists.newArrayList("java/lang/System", "out")));
+
+ assertEquals(3, analysis.methods.size());
+ assertThat(analysis.methods.get(0),
+ is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V")));
+ assertThat(analysis.methods.get(1),
+ is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V")));
+ assertThat(analysis.methods.get(2),
+ is(Lists.newArrayList("java/io/PrintStream", "print", "(Ljava/lang/String;)V")));
+ }
+
+ @SuppressWarnings("unused")
+ private static class StaticFields {
+ static final String sStringValue = "staticValue";
+ static final int sIntValue = 10;
+ static final int[] sIntArrayValue = {11, 12, 13};
+ static final String[] sStringArrayValue = {"a", "b", "c"};
+ }
+
+ @Test
+ public void testStaticValues()
+ throws CompilationFailedException, IOException, ExecutionException, CompilationException {
+ TrackAll analysis = runAnalysis(StaticFields.class);
+
+ assertThat(analysis.integers, hasItems(10, 11, 12, 13));
+ assertThat(analysis.strings, hasItems("staticValue", "a", "b", "c"));
+ assertThat(analysis.fields, is(Lists.newArrayList()));
+ assertThat(analysis.methods, is(Lists.newArrayList()));
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface IntAnnotation {
+ int value() default 10;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface OuterAnnotation {
+ IntAnnotation inner() default @IntAnnotation(11);
+ }
+
+ @IntAnnotation(42)
+ private static class Annotated {
+ @OuterAnnotation
+ Object defaultAnnotated = new Object();
+ @IntAnnotation(12)
+ Object withValueAnnotated = new Object();
+ @IntAnnotation(13)
+ static Object staticValueAnnotated = new Object();
+
+ @IntAnnotation(14)
+ public void annotatedPublic() {
+ }
+
+ @IntAnnotation(15)
+ public void annotatedPrivate() {
+ }
+ }
+
+ @Test
+ public void testAnnotations()
+ throws CompilationFailedException, IOException, ExecutionException, CompilationException {
+ TrackAll analysis = runAnalysis(IntAnnotation.class, OuterAnnotation.class, Annotated.class);
+
+ assertThat(analysis.integers, hasItems(10, 11, 12, 13, 14, 15, 42));
+ assertThat(analysis.strings, is(Sets.newHashSet()));
+ assertThat(analysis.fields, is(Lists.newArrayList()));
+ assertThat(analysis.methods, is(Lists.newArrayList()));
+ }
+
+ private static class ResourceClassToSkip {
+ int[] i = {100, 101, 102};
+ }
+
+ private static class ToProcess {
+ int[] i = {10, 11, 12};
+ String[] s = {"10", "11", "12"};
+ }
+
+ @Test
+ public void testWithSkippingSome()
+ throws ExecutionException, CompilationFailedException, CompilationException, IOException {
+ TrackAll analysis = runAnalysis(ResourceClassToSkip.class, ToProcess.class);
+
+ assertThat(analysis.integers, hasItems(10, 11, 12));
+ assertThat(analysis.strings, is(Sets.newHashSet("10", "11", "12")));
+ assertThat(analysis.fields, is(Lists.newArrayList()));
+ assertThat(analysis.methods, is(Lists.newArrayList()));
+ }
+
+ @Test
+ public void testPayloadBeforeFillArrayData() throws Exception {
+ SmaliBuilder builder = new SmaliBuilder("Test");
+ builder.addMainMethod(
+ 2,
+ "goto :start",
+ "",
+ ":array_data",
+ ".array-data 4",
+ " 4 5 6",
+ ".end array-data",
+ "",
+ ":start",
+ "const/4 v1, 3",
+ "new-array v0, v1, [I",
+ "fill-array-data v0, :array_data",
+ "return-object v0"
+ );
+ AndroidApp app =
+ AndroidApp.builder().addDexProgramData(builder.compile(), Origin.unknown()).build();
+ TrackAll analysis = runOnApp(app);
+
+ assertThat(analysis.integers, hasItems(4, 5, 6));
+ assertThat(analysis.strings, is(Sets.newHashSet()));
+ assertThat(analysis.fields, is(Lists.newArrayList()));
+ assertThat(analysis.methods, is(Lists.newArrayList()));
+ }
+
+ private TrackAll runAnalysis(Class<?>... classes)
+ throws IOException, CompilationException, ExecutionException, CompilationFailedException {
+ AndroidApp app = readClasses(classes);
+ return runOnApp(app);
+ }
+
+ private TrackAll runOnApp(AndroidApp app)
+ throws IOException, ExecutionException, CompilationFailedException, CompilationException {
+ AndroidApp outputApp = compileWithD8(app);
+ Path outputDex = tmp.newFolder().toPath().resolve("classes.dex");
+ outputApp.writeToDirectory(outputDex.getParent(), OutputMode.DexIndexed);
+
+ ProgramResourceProvider provider =
+ () -> Lists.newArrayList(ProgramResource.fromFile(ProgramResource.Kind.DEX, outputDex));
+ ResourceShrinker.Command command =
+ new ResourceShrinker.Builder().addProgramResourceProvider(provider).build();
+
+ TrackAll analysis = new TrackAll();
+ ResourceShrinker.run(command, analysis);
+ return analysis;
+ }
+}
\ No newline at end of file