/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.google.devrel.gmscore.tools.apk.arsc; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.Multimap; import com.google.devrel.gmscore.tools.apk.arsc.ArscBlamer.ResourceEntry; import com.google.devrel.gmscore.tools.apk.arsc.ResourceEntryStatsCollector.ResourceStatistics; import com.google.devrel.gmscore.tools.common.InjectedApplication; import com.google.devrel.gmscore.tools.common.flags.CommonParams; import com.google.inject.Inject; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.opencsv.CSVWriter; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import javax.annotation.Nullable; /** * Pulls useful information from an APK's resources.arsc file. This can be used to see the different * resource configurations in the APK, their size, and the entries in those configurations. * * <p>This can also be used to get a list of the different entry names, or to get a list of resource * entries for which no default value exists (baseless keys). * * <p>Example usage to save all resource configurations to a CSV file: * * <pre>ArscDumper.jar --apk=/apk_dir/my.apk --output=/csv_dir/my.csv --type=configs</pre> * * <p>This CSV could then be sorted by "Null Entries" in descending order to spot resource * configurations that could potentially be removed for large byte savings. */ public class ArscDumper { /** The type of dumper ArscDumper should output. */ private enum Type { CONFIGS, ENTRIES, BASELESS_KEYS } /** Columns for the CSV returned for resource configs. */ private static final List<String> CONFIGS_COLUMNS = ImmutableList.<String>builder() .add("Type") .add("Config") .add("Size") .add("Null Entries") .add("Entry Count") .add("Density") .add("Keys") .addAll(getConfigurationHeaders()) .build(); /** Columns for the CSV returned for entries / baseless keys. */ private static final List<String> ENTRIES_COLUMNS = ImmutableList.of( "Type", "Name", "Private Size", "Shared Size", "Proportional Size", "Config Count", "Configs"); private final ArscBlamer blamer; private final ResourceEntryStatsCollector collector; public static void main(String[] args) throws IOException { InjectedApplication application = new InjectedApplication.Builder(args) .withParameter(Params.class, CommonParams.class) .withModule(new ArscModule()) .build(); ArscDumper dumper = application.get(ArscDumper.class); Params params = application.get(Params.class); CommonParams commonParams = application.get(CommonParams.class); try (BufferedWriter writer = new BufferedWriter(getWriter(commonParams.getOutput()))) { switch (params.type) { case CONFIGS: dumper.dumpResourceConfigs(writer, params.keys); break; case ENTRIES: dumper.dumpEntries(writer); break; case BASELESS_KEYS: dumper.dumpBaselessKeys(writer); break; default: throw new UnsupportedOperationException( String.format("Missing implementation for type: %s.", params.type)); } } } /** * Creates a new {@link ArscDumper}. * * @param blamer The blamer to dump information from. * @param collector The collector to compute resource entry stats from. */ @Inject public ArscDumper(ArscBlamer blamer, ResourceEntryStatsCollector collector) { this.blamer = blamer; this.collector = collector; } /** * Writes a CSV dump of the resource configurations in the APK. * * @param writer The writer that will be used to write the CSV. * @param showKeys True if {@link ResourceEntry} keys should be shown in the output. * @throws IOException Thrown if {@code writer} could not be written to. */ public void dumpResourceConfigs(Writer writer, boolean showKeys) throws IOException { try (AutoCloseableCsvWriter csvWriter = new AutoCloseableCsvWriter(writer)) { csvWriter.writeNext(CONFIGS_COLUMNS); for (TypeChunk typeChunk : getTypeChunksBySparsity()) { csvWriter.writeNext(dumpResourceConfig(typeChunk, showKeys)); } } } /** * Returns a CSV row (as a list of strings) describing a particular resource configuration. If * showKeys is true, the "Keys" column will be populated with the keys of the resource entries in * that configuration. Otherwise, the "Keys" column will be blank. * * @param chunk The chunk to dump the configuration from. * @param showKeys True if "Keys" should contain the entries in {@code chunk}. * @return A CSV row describing a particular resource configuration. */ private List<String> dumpResourceConfig(TypeChunk chunk, boolean showKeys) { Map<Integer, TypeChunk.Entry> entries = chunk.getEntries(); double density = 1.0 * entries.size() / chunk.getTotalEntryCount(); int size = chunk.getOriginalChunkSize(); List<String> keyNames = new ArrayList<>(); if (showKeys) { for (TypeChunk.Entry entry : entries.values()) { keyNames.add(entry.key()); } } String keys = Joiner.on(' ').join(keyNames); return ImmutableList.<String>builder() .add(chunk.getTypeName()) .add(chunk.getConfiguration().toString()) .add(String.valueOf(size)) .add(String.valueOf(chunk.getTotalEntryCount() - entries.size())) .add(String.valueOf(entries.size())) .add(String.format("%.4f", density)) .add(keys) .addAll(getConfigurationParts(chunk.getConfiguration())) .build(); } /** * Returns a CSV dump of the resource entries in this APK. * * @param writer The writer that will be used to write the CSV. * @throws IOException Thrown if {@code writer} could not be written to. */ public void dumpEntries(Writer writer) throws IOException { dumpEntries(writer, blamer.getResourceEntries()); } /** * Returns a CSV dump of resource keys which have no default value ("any" configuration). * * @param writer The writer that will be used to write the CSV. * @throws IOException Thrown if {@code writer} could not be written to. */ public void dumpBaselessKeys(Writer writer) throws IOException { dumpEntries(writer, blamer.getBaselessKeys()); } private void dumpEntries(Writer writer, Multimap<ResourceEntry, TypeChunk.Entry> entries) throws IOException { collector.compute(); try (AutoCloseableCsvWriter csvWriter = new AutoCloseableCsvWriter(writer)) { csvWriter.writeNext(ENTRIES_COLUMNS); for (Entry<ResourceEntry, Collection<TypeChunk.Entry>> entry : entries.asMap().entrySet()) { csvWriter.writeNext(dumpEntry(entry, collector.getStats(entry.getKey()))); } } } private List<String> dumpEntry( Entry<ResourceEntry, ? extends Iterable<TypeChunk.Entry>> entry, ResourceStatistics stats) { ResourceEntry resourceEntry = entry.getKey(); Set<String> configParts = new TreeSet<>(); // Prevents duplicates of the same configuration for (TypeChunk.Entry chunkEntry : entry.getValue()) { configParts.add(chunkEntry.parent().getConfiguration().toString()); } return ImmutableList.<String>builder() .add(resourceEntry.typeName()) .add(resourceEntry.entryName()) .add(String.valueOf(stats.getPrivateSize())) .add(String.valueOf(stats.getSharedSize())) .add(new BigDecimal(stats.getProportionalSize()) .setScale(10, RoundingMode.HALF_EVEN) .toString()) .add(String.valueOf(configParts.size())) .add(Joiner.on(' ').join(configParts)) .build(); } /** Returns a list of {@link TypeChunk} ordered by number of resource entries it has. */ private List<TypeChunk> getTypeChunksBySparsity() { List<TypeChunk> result = new ArrayList<>(blamer.getTypeChunks()); Collections.sort(result, new Comparator<TypeChunk>() { @Override public int compare(TypeChunk o1, TypeChunk o2) { return Integer.valueOf(o1.getEntries().size()).compareTo(o2.getEntries().size()); } }); return result; } private static List<String> getConfigurationHeaders() { Builder<String> builder = ImmutableList.builder(); for (ResourceConfiguration.Type type : ResourceConfiguration.Type.values()) { builder.add(type.toString()); } return builder.build(); } private static List<String> getConfigurationParts(ResourceConfiguration configuration) { Map<ResourceConfiguration.Type, String> parts = configuration.toStringParts(); Builder<String> builder = ImmutableList.builder(); for (ResourceConfiguration.Type key : ResourceConfiguration.Type.values()) { builder.add(parts.containsKey(key) ? parts.get(key) : ""); } return builder.build(); } private static Writer getWriter(@Nullable File output) throws IOException { return (output == null) ? new OutputStreamWriter(System.out) : new FileWriter(output); } /** A wrapper around {@link CSVWriter} to allow {@link AutoCloseable}. */ private static class AutoCloseableCsvWriter implements AutoCloseable { private final CSVWriter csvWriter; public AutoCloseableCsvWriter(Writer writer) { csvWriter = new CSVWriter(writer); } public void writeNext(Collection<String> line) { csvWriter.writeNext(line.toArray(new String[line.size()])); } @Override public void close() throws IOException { csvWriter.close(); } } /** Provides params specific to {@link ArscDumper}. */ @Parameters(separators = " =") public static class Params { @Parameter(names = "--type", description = "The type of output to return. Values: [configs, entries, baseless_keys]") private Type type = Type.CONFIGS; @Parameter(names = "--keys", description = "If true, include all key names for the entries in configs") private Boolean keys = false; } }