12eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden/*
22eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * Copyright (C) 2010 The Android Open Source Project
32eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *
42eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * Licensed under the Apache License, Version 2.0 (the "License");
52eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * you may not use this file except in compliance with the License.
62eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * You may obtain a copy of the License at
72eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *
82eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *      http://www.apache.org/licenses/LICENSE-2.0
92eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *
102eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * Unless required by applicable law or agreed to in writing, software
112eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * distributed under the License is distributed on an "AS IS" BASIS,
122eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
132eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * See the License for the specific language governing permissions and
142eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * limitations under the License.
152eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden */
162eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
172eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFaddenpackage com.android.apkcheck;
182eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
192eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFaddenimport org.xml.sax.*;
202eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFaddenimport org.xml.sax.helpers.*;
21a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFaddenimport java.io.FileReader;
22a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFaddenimport java.io.IOException;
23a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFaddenimport java.io.Reader;
242eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFaddenimport java.util.ArrayList;
25a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFaddenimport java.util.HashSet;
262eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFaddenimport java.util.Iterator;
272eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
282eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
292eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden/**
302eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * Checks an APK's dependencies against the published API specification.
312eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *
322eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * We need to read two XML files (spec and APK) and perform some operations
332eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * on the elements.  The file formats are similar but not identical, so
342eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * we distill it down to common elements.
352eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *
362eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * We may also want to read some additional API lists representing
372eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * libraries that would be included with a "uses-library" directive.
382eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden *
392eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * For performance we want to allow processing of multiple APKs so
402eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden * we don't have to re-parse the spec file each time.
412eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden */
422eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFaddenpublic class ApkCheck {
432eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /* keep track of current APK file name, for error messages */
442eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static ApiList sCurrentApk;
452eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
462eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /* show warnings? */
472eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static boolean sShowWarnings = false;
482eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /* show errors? */
492eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static boolean sShowErrors = true;
502eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
51a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden    /* names of packages we're allowed to ignore */
52a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden    private static HashSet<String> sIgnorablePackages = new HashSet<String>();
53a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden
54a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden
552eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
562eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Program entry point.
572eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
582eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    public static void main(String[] args) {
592eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        ApiList apiDescr = new ApiList("public-api");
602eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
612eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        if (args.length < 2) {
622eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            usage();
632eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            return;
642eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
652eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
662eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        /* process args */
672eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        int idx;
682eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        for (idx = 0; idx < args.length; idx++) {
692eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            if (args[idx].equals("--help")) {
702eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                usage();
712eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                return;
722eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else if (args[idx].startsWith("--uses-library=")) {
732eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                String libName = args[idx].substring(args[idx].indexOf('=')+1);
742eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                if ("BUILTIN".equals(libName)) {
752eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    Reader reader = Builtin.getReader();
762eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    if (!parseXml(apiDescr, reader, "BUILTIN"))
772eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        return;
782eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                } else {
792eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    if (!parseApiDescr(apiDescr, libName))
802eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        return;
812eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                }
82a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden            } else if (args[idx].startsWith("--ignore-package=")) {
83a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                String pkgName = args[idx].substring(args[idx].indexOf('=')+1);
84a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                sIgnorablePackages.add(pkgName);
852eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else if (args[idx].equals("--warn")) {
862eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                sShowWarnings = true;
872eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else if (args[idx].equals("--no-warn")) {
882eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                sShowWarnings = false;
892eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else if (args[idx].equals("--error")) {
902eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                sShowErrors = true;
912eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else if (args[idx].equals("--no-error")) {
922eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                sShowErrors = false;
932eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
942eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else if (args[idx].startsWith("--")) {
952eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                if (args[idx].equals("--")) {
962eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    // remainder are filenames, even if they start with "--"
972eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    idx++;
982eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    break;
992eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                } else {
1002eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    // unknown option specified
1012eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    System.err.println("ERROR: unknown option " +
1022eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        args[idx] + " (use \"--help\" for usage info)");
1032eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    return;
1042eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                }
1052eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else {
1062eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                break;
1072eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
1082eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
1092eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        if (idx > args.length - 2) {
1102eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            usage();
1112eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            return;
1122eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
1132eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1142eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        /* parse base API description */
1152eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        if (!parseApiDescr(apiDescr, args[idx++]))
1162eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            return;
1172eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1182eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        /* "flatten" superclasses and interfaces */
1192eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        sCurrentApk = apiDescr;
1202eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        flattenInherited(apiDescr);
1212eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1222eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        /* walk through list of libs we want to scan */
1232eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        for ( ; idx < args.length; idx++) {
1242eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            ApiList apkDescr = new ApiList(args[idx]);
1252eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            sCurrentApk = apkDescr;
1262eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            boolean success = parseApiDescr(apkDescr, args[idx]);
1272eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            if (!success) {
1282eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                if (idx < args.length-1)
1292eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    System.err.println("Skipping...");
1302eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                continue;
1312eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
1322eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1332eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            check(apiDescr, apkDescr);
1342eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            System.out.println(args[idx] + ": summary: " +
1352eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                apkDescr.getErrorCount() + " errors, " +
1362eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                apkDescr.getWarningCount() + " warnings\n");
1372eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
1382eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
1392eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1402eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
1412eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Prints usage statement.
1422eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
1432eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    static void usage() {
1442eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("Android APK checker v1.0");
1452eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("Copyright (C) 2010 The Android Open Source Project\n");
1462eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("Usage: apkcheck [options] public-api.xml apk1.xml ...\n");
1472eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("Options:");
1482eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("  --help                  show this message");
1492eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("  --uses-library=lib.xml  load additional public API list");
150a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden        System.err.println("  --ignore-package=pkg    don't show errors for references to this package");
1512eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("  --[no-]warn             enable or disable display of warnings");
1522eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.err.println("  --[no-]error            enable or disable display of errors");
1532eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
1542eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1552eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
1562eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Opens the file and passes it to parseXml.
1572eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
1582eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * TODO: allow '-' as an alias for stdin?
1592eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
1602eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    static boolean parseApiDescr(ApiList apiList, String fileName) {
1612eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        boolean result = false;
1622eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1632eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        try {
1642eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            FileReader fileReader = new FileReader(fileName);
1652eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            result = parseXml(apiList, fileReader, fileName);
1662eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            fileReader.close();
1672eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        } catch (IOException ioe) {
1682eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            System.err.println("Error opening " + fileName);
1692eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
1702eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        return result;
1712eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
1722eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1732eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
1742eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Parses an XML file holding an API description.
1752eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
1762eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * @param fileReader Data source.
1772eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * @param apiList Container to add stuff to.
1782eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * @param fileName Input file name, only used for debug messages.
1792eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
1802eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    static boolean parseXml(ApiList apiList, Reader reader,
1812eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            String fileName) {
1822eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        //System.out.println("--- parsing " + fileName);
1832eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        try {
1842eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            XMLReader xmlReader = XMLReaderFactory.createXMLReader();
1852eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            ApiDescrHandler handler = new ApiDescrHandler(apiList);
1862eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            xmlReader.setContentHandler(handler);
1872eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            xmlReader.setErrorHandler(handler);
1882eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            xmlReader.parse(new InputSource(reader));
1892eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
1902eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            //System.out.println("--- parsing complete");
1912eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            //dumpApi(apiList);
1922eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            return true;
1932eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        } catch (SAXParseException ex) {
1942eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            System.err.println("Error parsing " + fileName + " line " +
1952eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                ex.getLineNumber() + ": " + ex.getMessage());
1962eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        } catch (Exception ex) {
1972eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            System.err.println("Error while reading " + fileName + ": " +
1982eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                ex.getMessage());
1992eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            ex.printStackTrace();
2002eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
2012eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2022eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        // failed
2032eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        return false;
2042eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
2052eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2062eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
2072eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Expands lists of fields and methods to recursively include superclass
2082eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * and interface entries.
2092eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
2102eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * The API description files have entries for every method a class
2112eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * declares, even if it's present in the superclass (e.g. toString()).
2122eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Removal of one of these methods doesn't constitute an API change,
2132eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * though, so if we don't find a method in a class we need to hunt
2142eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * through its superclasses.
2152eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
2162eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * We can walk up the hierarchy while analyzing the target APK,
2172eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * or we can "flatten" the methods declared by the superclasses and
2182eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * interfaces before we begin the analysis.  Expanding up front can be
2192eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * beneficial if we're analyzing lots of APKs in one go, but detrimental
2202eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * to startup time if we just want to look at one small APK.
2212eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
2222eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * It also means filling the field/method hash tables with lots of
2232eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * entries that never get used, possibly worsening the hash table
2242eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * hit rate.
2252eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
2262eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * We only need to do this for the public API list.  The dexdeps output
2272eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * doesn't have this sort of information anyway.
2282eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
2292eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    static void flattenInherited(ApiList pubList) {
2302eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<PackageInfo> pkgIter = pubList.getPackageIterator();
2312eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (pkgIter.hasNext()) {
2322eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            PackageInfo pubPkgInfo = pkgIter.next();
2332eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2342eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            Iterator<ClassInfo> classIter = pubPkgInfo.getClassIterator();
2352eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            while (classIter.hasNext()) {
2362eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                ClassInfo pubClassInfo = classIter.next();
2372eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2382eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                pubClassInfo.flattenClass(pubList);
2392eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
2402eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
2412eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
2422eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2432eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
2442eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Checks the APK against the public API.
2452eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
2462eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Run through and find the mismatches.
2472eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     *
2482eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * @return true if all is well
2492eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
2502eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    static boolean check(ApiList pubList, ApiList apkDescr) {
2512eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2522eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<PackageInfo> pkgIter = apkDescr.getPackageIterator();
2532eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (pkgIter.hasNext()) {
2542eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            PackageInfo apkPkgInfo = pkgIter.next();
2552eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            PackageInfo pubPkgInfo = pubList.getPackage(apkPkgInfo.getName());
2562eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            boolean badPackage = false;
2572eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2582eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            if (pubPkgInfo == null) {
2592eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                // "illegal package" not a tremendously useful message
2602eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                //apkError("Illegal package ref: " + apkPkgInfo.getName());
2612eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                badPackage = true;
2622eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
2632eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2642eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            Iterator<ClassInfo> classIter = apkPkgInfo.getClassIterator();
2652eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            while (classIter.hasNext()) {
2662eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                ClassInfo apkClassInfo = classIter.next();
2672eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2682eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                if (badPackage) {
269a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                    /*
270a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                     * The package is not present in the public API file,
271a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                     * but simply saying "bad package" isn't all that
272a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                     * useful, so we emit the names of each of the classes.
273a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                     */
274a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                    if (isIgnorable(apkPkgInfo)) {
275a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                        apkWarning("Ignoring class ref: " +
276a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                            apkPkgInfo.getName() + "." + apkClassInfo.getName());
277a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                    } else {
278a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                        apkError("Illegal class ref: " +
279a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                            apkPkgInfo.getName() + "." + apkClassInfo.getName());
280a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                    }
2812eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                } else {
2822eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    checkClass(pubPkgInfo, apkClassInfo);
2832eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                }
2842eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
2852eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
2862eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2872eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        return true;
2882eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
2892eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2902eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
2912eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Checks the class against the public API.  We check the class
2922eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * itself and then any fields and methods.
2932eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
2942eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    static boolean checkClass(PackageInfo pubPkgInfo, ClassInfo classInfo) {
2952eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2962eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        ClassInfo pubClassInfo = pubPkgInfo.getClass(classInfo.getName());
2972eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
2982eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        if (pubClassInfo == null) {
299a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden            if (isIgnorable(pubPkgInfo)) {
300a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                apkWarning("Ignoring class ref: " +
301a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden                    pubPkgInfo.getName() + "." + classInfo.getName());
302a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden            } else if (classInfo.hasNoFieldMethod()) {
3032eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                apkWarning("Hidden class referenced: " +
3042eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    pubPkgInfo.getName() + "." + classInfo.getName());
3052eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            } else {
3062eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                apkError("Illegal class ref: " +
3072eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    pubPkgInfo.getName() + "." + classInfo.getName());
3082eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                // could list specific fields/methods used
3092eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
3102eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            return false;
3112eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
3122eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3132eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        /*
3142eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden         * Check the contents of classInfo against pubClassInfo.
3152eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden         */
3162eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<FieldInfo> fieldIter = classInfo.getFieldIterator();
3172eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (fieldIter.hasNext()) {
3182eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            FieldInfo apkFieldInfo = fieldIter.next();
3192eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            String nameAndType = apkFieldInfo.getNameAndType();
3202eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            FieldInfo pubFieldInfo = pubClassInfo.getField(nameAndType);
3212eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            if (pubFieldInfo == null) {
3224fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                if (pubClassInfo.isEnum()) {
3232eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    apkWarning("Enum field ref: " + pubPkgInfo.getName() +
3242eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        "." + classInfo.getName() + "." + nameAndType);
3252eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                } else {
3262eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    apkError("Illegal field ref: " + pubPkgInfo.getName() +
3272eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        "." + classInfo.getName() + "." + nameAndType);
3282eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                }
3292eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
3302eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
3312eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3322eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<MethodInfo> methodIter = classInfo.getMethodIterator();
3332eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (methodIter.hasNext()) {
3342eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            MethodInfo apkMethodInfo = methodIter.next();
3352eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            String nameAndDescr = apkMethodInfo.getNameAndDescriptor();
3362eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            MethodInfo pubMethodInfo = pubClassInfo.getMethod(nameAndDescr);
3372eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            if (pubMethodInfo == null) {
3382eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                pubMethodInfo = pubClassInfo.getMethodIgnoringReturn(nameAndDescr);
3392eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                if (pubMethodInfo == null) {
3404fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                    if (pubClassInfo.isAnnotation()) {
3414fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                        apkWarning("Annotation method ref: " +
3424fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                            pubPkgInfo.getName() + "." + classInfo.getName() +
3434fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                            "." + nameAndDescr);
3444fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                    } else {
3454fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                        apkError("Illegal method ref: " + pubPkgInfo.getName() +
3464fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                            "." + classInfo.getName() + "." + nameAndDescr);
3474fbfbb3914cfcc6d2da41be818e76208f1e59866Andy McFadden                    }
3482eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                } else {
3492eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                    apkWarning("Possibly covariant method ref: " +
3502eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        pubPkgInfo.getName() + "." + classInfo.getName() +
3512eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                        "." + nameAndDescr);
3522eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                }
3532eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            }
3542eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
3552eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3562eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3572eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        return true;
3582eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
3592eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
360a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden    /**
361a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden     * Returns true if the package is in the "ignored" list.
362a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden     */
363a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden    static boolean isIgnorable(PackageInfo pkgInfo) {
364a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden        return sIgnorablePackages.contains(pkgInfo.getName());
365a4707b1709b7ca7391d6bdc2bb7cf7ff4a92c5a3Andy McFadden    }
3662eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3672eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
3682eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Prints a warning message about an APK problem.
3692eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
3702eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    public static void apkWarning(String msg) {
3712eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        if (sShowWarnings) {
3722eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            System.out.println("(warn) " + sCurrentApk.getDebugString() +
3732eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden                ": " + msg);
3742eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
3752eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        sCurrentApk.incrWarnings();
3762eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
3772eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3782eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
3792eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Prints an error message about an APK problem.
3802eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
3812eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    public static void apkError(String msg) {
3822eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        if (sShowErrors) {
3832eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            System.out.println(sCurrentApk.getDebugString() + ": " + msg);
3842eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
3852eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        sCurrentApk.incrErrors();
3862eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
3872eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
3882eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    /**
3892eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * Recursively dumps the contents of the API.  Sort order is not
3902eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     * specified.
3912eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden     */
3922eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static void dumpApi(ApiList apiList) {
3932eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<PackageInfo> iter = apiList.getPackageIterator();
3942eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (iter.hasNext()) {
3952eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            PackageInfo pkgInfo = iter.next();
3962eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            dumpPackage(pkgInfo);
3972eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
3982eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
3992eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
4002eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static void dumpPackage(PackageInfo pkgInfo) {
4012eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<ClassInfo> iter = pkgInfo.getClassIterator();
4022eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.out.println("PACKAGE " + pkgInfo.getName());
4032eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (iter.hasNext()) {
4042eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            ClassInfo classInfo = iter.next();
4052eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            dumpClass(classInfo);
4062eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
4072eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
4082eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
4092eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static void dumpClass(ClassInfo classInfo) {
4102eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.out.println(" CLASS " + classInfo.getName());
4112eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<FieldInfo> fieldIter = classInfo.getFieldIterator();
4122eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (fieldIter.hasNext()) {
4132eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            FieldInfo fieldInfo = fieldIter.next();
4142eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            dumpField(fieldInfo);
4152eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
4162eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        Iterator<MethodInfo> methIter = classInfo.getMethodIterator();
4172eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        while (methIter.hasNext()) {
4182eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            MethodInfo methInfo = methIter.next();
4192eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden            dumpMethod(methInfo);
4202eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        }
4212eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
4222eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
4232eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static void dumpMethod(MethodInfo methInfo) {
4242eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.out.println("  METHOD " + methInfo.getNameAndDescriptor());
4252eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
4262eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
4272eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    private static void dumpField(FieldInfo fieldInfo) {
4282eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden        System.out.println("  FIELD " + fieldInfo.getNameAndType());
4292eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden    }
4302eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden}
4312eceaea745773bb654bb4e52c00cafdedc68c0acAndy McFadden
432