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 org.gradle.api.GradleException;
019import org.slf4j.Logger;
020
021import java.io.IOException;
022
023/**
024 * An object that can produce formatted output from a {@link PackageTree} instance.
025 */
026public class CountReporter {
027    /**
028     * The maximum number of method refs and field refs allowed in a single Dex
029     * file.
030     */
031    private static final int MAX_DEX_REFS = 0xFFFF; // 65535
032
033    private final PackageTree packageTree;
034    private final String variantName;
035    private final Logger logger;
036    private final PrintOptions options;
037    private final String inputRepresentation;
038    private final boolean isInstantRun;
039
040    public CountReporter(
041            PackageTree packageTree,
042            String variantName,
043            Logger logger,
044            PrintOptions options,
045            String inputRepresentation,
046            boolean isInstantRun) {
047        this.packageTree = packageTree;
048        this.variantName = variantName;
049        this.logger = logger;
050        this.options = options;
051        this.inputRepresentation = inputRepresentation;
052        this.isInstantRun = isInstantRun;
053    }
054
055    public void report() throws IOException {
056        try {
057            printPreamble();
058            printSummary();
059            printTaskDiagnosticData();
060            failBuildMaxMethods();
061        } catch (DexCountException e) {
062            logger.error("Error counting dex methods. Please contact the developer at https://github.com/KeepSafe/dexcount-gradle-plugin/issues", e);
063        }
064    }
065
066    private void printPreamble() {
067        if (options.getPrintHeader()) {
068            String projectName = getClass().getPackage().getImplementationTitle();
069            String projectVersion = getClass().getPackage().getImplementationVersion();
070
071            logger.warn("Dexcount name:    {}", projectName);
072            logger.warn("Dexcount version: {}", projectVersion);
073            logger.warn("Dexcount input:   {}", inputRepresentation);
074        }
075    }
076
077    private String percentUsed(int count) {
078        double used = ((double) count / MAX_DEX_REFS) * 100.0;
079        return String.format("%.2f", used);
080    }
081
082    private void printSummary() {
083        if (isInstantRun) {
084            logger.warn("Warning: Instant Run build detected!  Instant Run does not run Proguard; method counts may be inaccurate.");
085        }
086
087        String percentMethodsUsed = percentUsed(packageTree.getMethodCount());
088        String percentFieldsUsed = percentUsed(packageTree.getFieldCount());
089        String percentClassesUsed = percentUsed(packageTree.getClassCount());
090
091        int methodsRemaining = Math.max(MAX_DEX_REFS - packageTree.getMethodCount(), 0);
092        int fieldsRemaining = Math.max(MAX_DEX_REFS - packageTree.getFieldCount(), 0);
093        int classesRemaining = Math.max(MAX_DEX_REFS - packageTree.getClassCount(), 0);
094
095        int methodCount, fieldCount, classCount;
096        if (options.isAndroidProject()) {
097            methodCount = packageTree.getMethodCount();
098            fieldCount = packageTree.getFieldCount();
099            classCount = packageTree.getClassCount();
100        } else {
101            methodCount = packageTree.getMethodCountDeclared();
102            fieldCount = packageTree.getFieldCountDeclared();
103            classCount = packageTree.getClassCountDeclared();
104        }
105
106        logger.warn("Total methods in " + inputRepresentation + ": " + methodCount + " (" + percentMethodsUsed + "% used)");
107        logger.warn("Total fields in " + inputRepresentation + ": " + fieldCount + " (" + percentFieldsUsed + "% used)");
108        logger.warn("Total classes in " + inputRepresentation + ": " + classCount + " (" + percentClassesUsed + "% used)");
109
110        if (options.isAndroidProject()) {
111            logger.warn("Methods remaining in " + inputRepresentation + ": " + methodsRemaining);
112            logger.warn("Fields remaining in " + inputRepresentation + ": " + fieldsRemaining);
113            logger.warn("Classes remaining in " + inputRepresentation + ": " + classesRemaining);
114        }
115
116        if (options.getTeamCityIntegration() || (options.getTeamCitySlug() != null && options.getTeamCitySlug().length() > 0)) {
117            String slug = "Dexcount";
118            if (options.getTeamCitySlug() != null) {
119                slug += "_" + options.getTeamCitySlug().replace(' ', '_');
120            }
121            String prefix = slug + "_" + variantName;
122
123            /*
124             * Reports to Team City statistic value
125             * Doc: https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ReportingBuildStatistics
126             */
127            logger.warn(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "ClassCount", packageTree.getClassCount()));
128            logger.warn(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "MethodCount", packageTree.getMethodCount()));
129            logger.warn(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "FieldCount", packageTree.getFieldCount()));
130        }
131    }
132
133    private void printTaskDiagnosticData() throws IOException {
134        StringBuilder strBuilder = new StringBuilder();
135        packageTree.print(strBuilder, options.getOutputFormat(), options);
136
137        if (options.isVerbose()) {
138            logger.warn(strBuilder.toString());
139        } else {
140            logger.info(strBuilder.toString());
141        }
142    }
143
144    private void failBuildMaxMethods() {
145        if (options.getMaxMethodCount() > 0 && packageTree.getMethodCount() > options.getMaxMethodCount()) {
146            String message = String.format("The current APK has %d methods, the current max is: %d.", packageTree.getMethodCount(), options.getMaxMethodCount());
147            throw new GradleException(message);
148        }
149    }
150}