001/*
002 * Copyright (C) 2015-2019 KeepSafe Software
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package com.getkeepsafe.dexcount;
017
018import com.getkeepsafe.dexcount.colors.Color;
019import com.getkeepsafe.dexcount.colors.Styleable;
020import org.gradle.api.GradleException;
021import org.gradle.api.logging.LogLevel;
022
023import java.io.IOException;
024
025/**
026 * An object that can produce formatted output from a {@link PackageTree} instance.
027 */
028public class CountReporter {
029    /**
030     * The maximum number of method refs and field refs allowed in a single Dex
031     * file.
032     */
033    private static final int MAX_DEX_REFS = 0xFFFF; // 65535
034
035    private final PackageTree packageTree;
036    private final String variantName;
037    private final Styleable styleable;
038    private final PrintOptions options;
039    private final String inputRepresentation;
040    private final boolean isInstantRun;
041
042    public CountReporter(
043            PackageTree packageTree,
044            String variantName,
045            Styleable styleable,
046            PrintOptions options,
047            String inputRepresentation,
048            boolean isInstantRun) {
049        this.packageTree = packageTree;
050        this.variantName = variantName;
051        this.styleable = styleable;
052        this.options = options;
053        this.inputRepresentation = inputRepresentation;
054        this.isInstantRun = isInstantRun;
055    }
056
057    public void report() throws IOException {
058        try {
059            printPreamble();
060            printSummary();
061            printTaskDiagnosticData();
062            failBuildMaxMethods();
063        } catch (DexCountException e) {
064            styleable.withStyledOutput(Color.RED, LogLevel.ERROR, out -> {
065                out.println("Error counting dex methods. Please contact the developer at https://github.com/KeepSafe/dexcount-gradle-plugin/issues");
066                e.printStackTrace(out);
067            });
068        }
069    }
070
071    private void printPreamble() throws IOException {
072        if (options.getPrintHeader()) {
073            String projectName = getClass().getPackage().getImplementationTitle();
074            String projectVersion = getClass().getPackage().getImplementationVersion();
075
076            styleable.withStyledOutput(Color.DEFAULT, out -> {
077                out.println("Dexcount name:    " + projectName);
078                out.println("Dexcount version: " + projectVersion);
079                out.println("Dexcount input:   " + inputRepresentation);
080            });
081        }
082    }
083
084    private String percentUsed(int count) {
085        double used = ((double) count / MAX_DEX_REFS) * 100.0;
086        return String.format("%.2f", used);
087    }
088
089    private void printSummary() throws IOException {
090        if (isInstantRun) {
091            styleable.withStyledOutput(Color.RED, out -> {
092                out.println("Warning: Instant Run build detected!  Instant Run does not run Proguard; method counts may be inaccurate.");
093            });
094        }
095
096        Color color = packageTree.getMethodCount() < 50000 ? Color.GREEN : Color.YELLOW;
097
098        styleable.withStyledOutput(color, out -> {
099            String percentMethodsUsed = percentUsed(packageTree.getMethodCount());
100            String percentFieldsUsed = percentUsed(packageTree.getFieldCount());
101            String percentClassesUsed = percentUsed(packageTree.getClassCount());
102
103            int methodsRemaining = Math.max(MAX_DEX_REFS - packageTree.getMethodCount(), 0);
104            int fieldsRemaining = Math.max(MAX_DEX_REFS - packageTree.getFieldCount(), 0);
105            int classesRemaining = Math.max(MAX_DEX_REFS - packageTree.getClassCount(), 0);
106
107            int methodCount, fieldCount, classCount;
108            if (options.isAndroidProject()) {
109                methodCount = packageTree.getMethodCount();
110                fieldCount = packageTree.getFieldCount();
111                classCount = packageTree.getClassCount();
112            } else {
113                methodCount = packageTree.getMethodCountDeclared();
114                fieldCount = packageTree.getFieldCountDeclared();
115                classCount = packageTree.getClassCountDeclared();
116            }
117
118            out.println("Total methods in " + inputRepresentation + ": " + methodCount + " (" + percentMethodsUsed + "% used)");
119            out.println("Total fields in " + inputRepresentation + ": " + fieldCount + " (" + percentFieldsUsed + "% used)");
120            out.println("Total classes in " + inputRepresentation + ": " + classCount + " (" + percentClassesUsed + "% used)");
121
122            if (options.isAndroidProject()) {
123                out.println("Methods remaining in " + inputRepresentation + ": " + methodsRemaining);
124                out.println("Fields remaining in " + inputRepresentation + ": " + fieldsRemaining);
125                out.println("Classes remaining in " + inputRepresentation + ": " + classesRemaining);
126            }
127        });
128
129        if (options.getTeamCityIntegration() || (options.getTeamCitySlug() != null && options.getTeamCitySlug().length() > 0)) {
130            styleable.withStyledOutput(Color.DEFAULT, out -> {
131                String slug = "Dexcount";
132                if (options.getTeamCitySlug() != null) {
133                    slug += "_" + options.getTeamCitySlug().replace(' ', '_');
134                }
135                String prefix = slug + "_" + variantName;
136
137                /*
138                 * Reports to Team City statistic value
139                 * Doc: https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ReportingBuildStatistics
140                 */
141                out.println(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "ClassCount", packageTree.getClassCount()));
142                out.println(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "MethodCount", packageTree.getMethodCount()));
143                out.println(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "FieldCount", packageTree.getFieldCount()));
144            });
145        }
146    }
147
148    private void printTaskDiagnosticData() throws IOException {
149        // Log the entire package list/tree at LogLevel.DEBUG, unless
150        // verbose is enabled (in which case use the default log level).
151        LogLevel level = options.isVerbose() ? null : LogLevel.DEBUG;
152
153        styleable.withStyledOutput(Color.YELLOW, level, out -> {
154            StringBuilder strBuilder = new StringBuilder();
155            packageTree.print(strBuilder, options.getOutputFormat(), options);
156
157            out.format(strBuilder.toString());
158        });
159    }
160
161    private void failBuildMaxMethods() {
162        if (options.getMaxMethodCount() > 0 && packageTree.getMethodCount() > options.getMaxMethodCount()) {
163            String message = String.format("The current APK has %d methods, the current max is: %d.", packageTree.getMethodCount(), options.getMaxMethodCount());
164            throw new GradleException(message);
165        }
166    }
167}