|  | // 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.dex.ApplicationReader; | 
|  | import com.android.tools.r8.dex.code.DexConst; | 
|  | import com.android.tools.r8.dex.code.DexConst16; | 
|  | import com.android.tools.r8.dex.code.DexConst4; | 
|  | import com.android.tools.r8.dex.code.DexConstHigh16; | 
|  | import com.android.tools.r8.dex.code.DexConstString; | 
|  | import com.android.tools.r8.dex.code.DexConstStringJumbo; | 
|  | import com.android.tools.r8.dex.code.DexConstWide16; | 
|  | import com.android.tools.r8.dex.code.DexConstWide32; | 
|  | import com.android.tools.r8.dex.code.DexFillArrayData; | 
|  | import com.android.tools.r8.dex.code.DexFillArrayDataPayload; | 
|  | import com.android.tools.r8.dex.code.DexFormat35c; | 
|  | import com.android.tools.r8.dex.code.DexFormat3rc; | 
|  | import com.android.tools.r8.dex.code.DexInstruction; | 
|  | import com.android.tools.r8.dex.code.DexInvokeDirect; | 
|  | import com.android.tools.r8.dex.code.DexInvokeDirectRange; | 
|  | import com.android.tools.r8.dex.code.DexInvokeInterface; | 
|  | import com.android.tools.r8.dex.code.DexInvokeInterfaceRange; | 
|  | import com.android.tools.r8.dex.code.DexInvokeStatic; | 
|  | import com.android.tools.r8.dex.code.DexInvokeStaticRange; | 
|  | import com.android.tools.r8.dex.code.DexInvokeSuper; | 
|  | import com.android.tools.r8.dex.code.DexInvokeSuperRange; | 
|  | import com.android.tools.r8.dex.code.DexInvokeVirtual; | 
|  | import com.android.tools.r8.dex.code.DexInvokeVirtualRange; | 
|  | import com.android.tools.r8.dex.code.DexNewArray; | 
|  | import com.android.tools.r8.dex.code.DexSget; | 
|  | import com.android.tools.r8.dex.code.DexSgetBoolean; | 
|  | import com.android.tools.r8.dex.code.DexSgetByte; | 
|  | import com.android.tools.r8.dex.code.DexSgetChar; | 
|  | import com.android.tools.r8.dex.code.DexSgetObject; | 
|  | import com.android.tools.r8.dex.code.DexSgetShort; | 
|  | import com.android.tools.r8.dex.code.DexSgetWide; | 
|  | 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.keepanno.annotations.KeepForApi; | 
|  | import com.android.tools.r8.references.ClassReference; | 
|  | import com.android.tools.r8.references.MethodReference; | 
|  | 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.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. | 
|  | */ | 
|  |  | 
|  | // TODO(b/121121779) Remove keep if possible. | 
|  | @Deprecated | 
|  | @KeepForApi | 
|  | final public class ResourceShrinker { | 
|  |  | 
|  | @KeepForApi | 
|  | public final static class Command extends BaseCommand { | 
|  |  | 
|  | Command(AndroidApp app) { | 
|  | super(app); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | InternalOptions getInternalOptions() { | 
|  | return new InternalOptions(); | 
|  | } | 
|  | } | 
|  |  | 
|  | @KeepForApi | 
|  | public final 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. | 
|  | */ | 
|  | @KeepForApi | 
|  | public 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); | 
|  |  | 
|  | default void startMethodVisit(MethodReference methodReference) {} | 
|  |  | 
|  | default void endMethodVisit(MethodReference methodReference) {} | 
|  |  | 
|  | default void startClassVisit(ClassReference classReference) {} | 
|  |  | 
|  | default void endClassVisit(ClassReference classReference) {} | 
|  | } | 
|  |  | 
|  | 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() { | 
|  | callback.startClassVisit(classDef.getClassReference()); | 
|  | if (!callback.shouldProcess(classDef.type.getInternalName())) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | for (DexEncodedField field : classDef.staticFields()) { | 
|  | DexValue staticValue = field.getStaticValue(); | 
|  | if (staticValue != null) { | 
|  | processFieldValue(staticValue); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (DexEncodedMethod method : classDef.allMethodsSorted()) { | 
|  |  | 
|  | callback.startMethodVisit(method.getReference().asMethodReference()); | 
|  | processMethod(method); | 
|  | callback.endMethodVisit(method.getReference().asMethodReference()); | 
|  | } | 
|  |  | 
|  | if (classDef.hasClassOrMemberAnnotations()) { | 
|  | processAnnotations(classDef); | 
|  | } | 
|  | callback.endClassVisit(classDef.getClassReference()); | 
|  | } | 
|  |  | 
|  | private void processFieldValue(DexValue value) { | 
|  | switch (value.getValueKind()) { | 
|  | case ARRAY: | 
|  | for (DexValue elementValue : value.asDexValueArray().getValues()) { | 
|  | if (elementValue.isDexValueInt()) { | 
|  | callback.referencedInt(elementValue.asDexValueInt().getValue()); | 
|  | } | 
|  | } | 
|  | break; | 
|  |  | 
|  | case INT: | 
|  | callback.referencedInt(value.asDexValueInt().getValue()); | 
|  | break; | 
|  |  | 
|  | case STRING: | 
|  | callback.referencedString(value.asDexValueString().value.toString()); | 
|  | break; | 
|  |  | 
|  | default: | 
|  | // Intentionally empty. | 
|  | } | 
|  | } | 
|  |  | 
|  | 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<DexFillArrayDataPayload> payloads = Lists.newArrayList(); | 
|  |  | 
|  | DexInstruction[] instructions = implementation.asDexCode().instructions; | 
|  | int current = 0; | 
|  | while (current < instructions.length) { | 
|  | DexInstruction 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 DexFillArrayData) { | 
|  | processFillArray(instructions, current, methodIntArrayPayloadOffsets); | 
|  | } else if (instruction instanceof DexFillArrayDataPayload) { | 
|  | payloads.add((DexFillArrayDataPayload) instruction); | 
|  | } | 
|  | current++; | 
|  | } | 
|  |  | 
|  | for (DexFillArrayDataPayload payload : payloads) { | 
|  | if (isIntArrayPayload(payload, methodIntArrayPayloadOffsets)) { | 
|  | processIntArrayPayload(payload); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private void processAnnotations(DexProgramClass classDef) { | 
|  | Stream<DexAnnotation> classAnnotations = classDef.annotations().stream(); | 
|  | Stream<DexAnnotation> fieldAnnotations = | 
|  | Streams.stream(classDef.fields()) | 
|  | .filter(DexEncodedField::hasAnnotations) | 
|  | .flatMap(f -> f.annotations().stream()); | 
|  | Stream<DexAnnotation> methodAnnotations = | 
|  | Streams.stream(classDef.methods()) | 
|  | .filter(DexEncodedMethod::hasAnyAnnotations) | 
|  | .flatMap(m -> m.annotations().stream()); | 
|  |  | 
|  | Streams.concat(classAnnotations, fieldAnnotations, methodAnnotations) | 
|  | .forEach( | 
|  | annotation -> { | 
|  | for (DexAnnotationElement element : annotation.annotation.elements) { | 
|  | DexValue value = element.value; | 
|  | processAnnotationValue(value); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | private void processIntArrayPayload(DexInstruction instruction) { | 
|  | DexFillArrayDataPayload payload = (DexFillArrayDataPayload) 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( | 
|  | DexInstruction instruction, Set<Integer> methodIntArrayPayloadOffsets) { | 
|  | if (!(instruction instanceof DexFillArrayDataPayload)) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | DexFillArrayDataPayload payload = (DexFillArrayDataPayload) instruction; | 
|  | return methodIntArrayPayloadOffsets.contains(payload.getOffset()); | 
|  | } | 
|  |  | 
|  | private void processFillArray( | 
|  | DexInstruction[] instructions, int current, Set<Integer> methodIntArrayPayloadOffsets) { | 
|  | DexFillArrayData fillArrayData = (DexFillArrayData) instructions[current]; | 
|  | if (current > 0 && instructions[current - 1] instanceof DexNewArray) { | 
|  | DexNewArray newArray = (DexNewArray) 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.getOffset()); | 
|  | } | 
|  |  | 
|  | private void processAnnotationValue(DexValue value) { | 
|  | switch (value.getValueKind()) { | 
|  | case ANNOTATION: | 
|  | for (DexAnnotationElement element : value.asDexValueAnnotation().value.elements) { | 
|  | processAnnotationValue(element.value); | 
|  | } | 
|  | break; | 
|  |  | 
|  | case ARRAY: | 
|  | for (DexValue elementValue : value.asDexValueArray().getValues()) { | 
|  | processAnnotationValue(elementValue); | 
|  | } | 
|  | break; | 
|  |  | 
|  | case INT: | 
|  | callback.referencedInt(value.asDexValueInt().value); | 
|  | break; | 
|  |  | 
|  | case STRING: | 
|  | callback.referencedString(value.asDexValueString().value.toString()); | 
|  | break; | 
|  |  | 
|  | default: | 
|  | // Intentionally empty. | 
|  | } | 
|  | } | 
|  |  | 
|  | private boolean isIntConstInstruction(DexInstruction instruction) { | 
|  | int opcode = instruction.getOpcode(); | 
|  | return opcode == DexConst4.OPCODE | 
|  | || opcode == DexConst16.OPCODE | 
|  | || opcode == DexConst.OPCODE | 
|  | || opcode == DexConstWide32.OPCODE | 
|  | || opcode == DexConstHigh16.OPCODE | 
|  | || opcode == DexConstWide16.OPCODE; | 
|  | } | 
|  |  | 
|  | private void processIntConstInstruction(DexInstruction 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(DexInstruction instruction) { | 
|  | int opcode = instruction.getOpcode(); | 
|  | return opcode == DexConstString.OPCODE || opcode == DexConstStringJumbo.OPCODE; | 
|  | } | 
|  |  | 
|  | private void processStringConstantInstruction(DexInstruction instruction) { | 
|  | assert isStringConstInstruction(instruction); | 
|  |  | 
|  | String constantValue; | 
|  | if (instruction instanceof DexConstString) { | 
|  | DexConstString constString = (DexConstString) instruction; | 
|  | constantValue = constString.getString().toString(); | 
|  | } else if (instruction instanceof DexConstStringJumbo) { | 
|  | DexConstStringJumbo constStringJumbo = (DexConstStringJumbo) instruction; | 
|  | constantValue = constStringJumbo.getString().toString(); | 
|  | } else { | 
|  | throw new AssertionError("Not a string constant instruction."); | 
|  | } | 
|  |  | 
|  | callback.referencedString(constantValue); | 
|  | } | 
|  |  | 
|  | private boolean isGetStatic(DexInstruction instruction) { | 
|  | int opcode = instruction.getOpcode(); | 
|  | return opcode == DexSget.OPCODE | 
|  | || opcode == DexSgetBoolean.OPCODE | 
|  | || opcode == DexSgetByte.OPCODE | 
|  | || opcode == DexSgetChar.OPCODE | 
|  | || opcode == DexSgetObject.OPCODE | 
|  | || opcode == DexSgetShort.OPCODE | 
|  | || opcode == DexSgetWide.OPCODE; | 
|  | } | 
|  |  | 
|  | private void processGetStatic(DexInstruction instruction) { | 
|  | assert isGetStatic(instruction); | 
|  |  | 
|  | DexField field; | 
|  | if (instruction instanceof DexSget) { | 
|  | DexSget sget = (DexSget) instruction; | 
|  | field = sget.getField(); | 
|  | } else if (instruction instanceof DexSgetBoolean) { | 
|  | DexSgetBoolean sgetBoolean = (DexSgetBoolean) instruction; | 
|  | field = sgetBoolean.getField(); | 
|  | } else if (instruction instanceof DexSgetByte) { | 
|  | DexSgetByte sgetByte = (DexSgetByte) instruction; | 
|  | field = sgetByte.getField(); | 
|  | } else if (instruction instanceof DexSgetChar) { | 
|  | DexSgetChar sgetChar = (DexSgetChar) instruction; | 
|  | field = sgetChar.getField(); | 
|  | } else if (instruction instanceof DexSgetObject) { | 
|  | DexSgetObject sgetObject = (DexSgetObject) instruction; | 
|  | field = sgetObject.getField(); | 
|  | } else if (instruction instanceof DexSgetShort) { | 
|  | DexSgetShort sgetShort = (DexSgetShort) instruction; | 
|  | field = sgetShort.getField(); | 
|  | } else if (instruction instanceof DexSgetWide) { | 
|  | DexSgetWide sgetWide = (DexSgetWide) instruction; | 
|  | field = sgetWide.getField(); | 
|  | } else { | 
|  | throw new AssertionError("Not a get static instruction"); | 
|  | } | 
|  |  | 
|  | callback.referencedStaticField(field.holder.getInternalName(), field.name.toString()); | 
|  | } | 
|  |  | 
|  | private boolean isInvokeInstruction(DexInstruction instruction) { | 
|  | int opcode = instruction.getOpcode(); | 
|  | return opcode == DexInvokeVirtual.OPCODE | 
|  | || opcode == DexInvokeSuper.OPCODE | 
|  | || opcode == DexInvokeDirect.OPCODE | 
|  | || opcode == DexInvokeStatic.OPCODE | 
|  | || opcode == DexInvokeInterface.OPCODE; | 
|  | } | 
|  |  | 
|  | private void processInvokeInstruction(DexInstruction instruction) { | 
|  | assert isInvokeInstruction(instruction); | 
|  |  | 
|  | DexFormat35c ins35c = (DexFormat35c) instruction; | 
|  | DexMethod method = (DexMethod) ins35c.BBBB; | 
|  |  | 
|  | callback.referencedMethod( | 
|  | method.holder.getInternalName(), | 
|  | method.name.toString(), | 
|  | method.proto.toDescriptorString()); | 
|  | } | 
|  |  | 
|  | private boolean isInvokeRangeInstruction(DexInstruction instruction) { | 
|  | int opcode = instruction.getOpcode(); | 
|  | return opcode == DexInvokeVirtualRange.OPCODE | 
|  | || opcode == DexInvokeSuperRange.OPCODE | 
|  | || opcode == DexInvokeDirectRange.OPCODE | 
|  | || opcode == DexInvokeStaticRange.OPCODE | 
|  | || opcode == DexInvokeInterfaceRange.OPCODE; | 
|  | } | 
|  |  | 
|  | private void processInvokeRangeInstruction(DexInstruction instruction) { | 
|  | assert isInvokeRangeInstruction(instruction); | 
|  |  | 
|  | DexFormat3rc ins3rc = (DexFormat3rc) instruction; | 
|  | DexMethod method = (DexMethod) ins3rc.BBBB; | 
|  |  | 
|  | callback.referencedMethod( | 
|  | method.holder.getInternalName(), | 
|  | method.name.toString(), | 
|  | method.proto.toDescriptorString()); | 
|  | } | 
|  | } | 
|  |  | 
|  | @SuppressWarnings("RedundantThrows") | 
|  | public static void run(Command command, ReferenceChecker callback) | 
|  | throws IOException, ExecutionException { | 
|  | runForTesting(command.getInputApp(), command.getInternalOptions(), callback); | 
|  | } | 
|  |  | 
|  | public static void runForTesting( | 
|  | AndroidApp inputApp, InternalOptions options, ReferenceChecker callback) | 
|  | throws IOException, ExecutionException { | 
|  | Timing timing = new Timing("resource shrinker analyzer"); | 
|  | DexApplication dexApplication = new ApplicationReader(inputApp, options, timing).read(); | 
|  | for (DexProgramClass programClass : dexApplication.classes()) { | 
|  | new DexClassUsageVisitor(programClass, callback).visit(); | 
|  | } | 
|  | } | 
|  | } |