com.google.devrel.gmscore.tools.apk.arsc.ArscDumper.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devrel.gmscore.tools.apk.arsc.ArscDumper.java

Source

/*
 * 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;
    }
}