Home | History | Annotate | Download | only in checks
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tools.lint.checks;
     18 
     19 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_MANIFEST_XML;
     20 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI;
     21 import static com.android.tools.lint.detector.api.LintConstants.ATTR_ICON;
     22 import static com.android.tools.lint.detector.api.LintConstants.DOT_9PNG;
     23 import static com.android.tools.lint.detector.api.LintConstants.DOT_GIF;
     24 import static com.android.tools.lint.detector.api.LintConstants.DOT_JPG;
     25 import static com.android.tools.lint.detector.api.LintConstants.DOT_PNG;
     26 import static com.android.tools.lint.detector.api.LintConstants.DOT_XML;
     27 import static com.android.tools.lint.detector.api.LintConstants.DRAWABLE_FOLDER;
     28 import static com.android.tools.lint.detector.api.LintConstants.DRAWABLE_HDPI;
     29 import static com.android.tools.lint.detector.api.LintConstants.DRAWABLE_LDPI;
     30 import static com.android.tools.lint.detector.api.LintConstants.DRAWABLE_MDPI;
     31 import static com.android.tools.lint.detector.api.LintConstants.DRAWABLE_RESOURCE_PREFIX;
     32 import static com.android.tools.lint.detector.api.LintConstants.DRAWABLE_XHDPI;
     33 import static com.android.tools.lint.detector.api.LintConstants.RES_FOLDER;
     34 import static com.android.tools.lint.detector.api.LintConstants.TAG_APPLICATION;
     35 import static com.android.tools.lint.detector.api.LintUtils.endsWith;
     36 
     37 import com.android.tools.lint.detector.api.Category;
     38 import com.android.tools.lint.detector.api.Context;
     39 import com.android.tools.lint.detector.api.Detector;
     40 import com.android.tools.lint.detector.api.Issue;
     41 import com.android.tools.lint.detector.api.LintUtils;
     42 import com.android.tools.lint.detector.api.Location;
     43 import com.android.tools.lint.detector.api.Scope;
     44 import com.android.tools.lint.detector.api.Severity;
     45 import com.android.tools.lint.detector.api.Speed;
     46 import com.android.tools.lint.detector.api.XmlContext;
     47 import com.google.common.io.Files;
     48 
     49 import org.w3c.dom.Element;
     50 
     51 import java.awt.Dimension;
     52 import java.awt.image.BufferedImage;
     53 import java.io.File;
     54 import java.io.IOException;
     55 import java.util.ArrayList;
     56 import java.util.Collection;
     57 import java.util.Collections;
     58 import java.util.Comparator;
     59 import java.util.HashMap;
     60 import java.util.HashSet;
     61 import java.util.Iterator;
     62 import java.util.List;
     63 import java.util.Map;
     64 import java.util.Map.Entry;
     65 import java.util.Set;
     66 import java.util.regex.Matcher;
     67 import java.util.regex.Pattern;
     68 
     69 import javax.imageio.ImageIO;
     70 import javax.imageio.ImageReader;
     71 import javax.imageio.stream.ImageInputStream;
     72 
     73 /**
     74  * Checks for common icon problems, such as wrong icon sizes, placing icons in the
     75  * density independent drawable folder, etc.
     76  */
     77 public class IconDetector extends Detector implements Detector.XmlScanner {
     78 
     79     private static final boolean INCLUDE_LDPI;
     80     static {
     81         boolean includeLdpi = false;
     82 
     83         String value = System.getenv("ANDROID_LINT_INCLUDE_LDPI"); //$NON-NLS-1$
     84         if (value != null) {
     85             includeLdpi = Boolean.valueOf(value);
     86         }
     87         INCLUDE_LDPI = includeLdpi;
     88     }
     89 
     90     /** Pattern for the expected density folders to be found in the project */
     91     private static final Pattern DENSITY_PATTERN = Pattern.compile(
     92             "^drawable-(nodpi|xhdpi|hdpi|mdpi"            //$NON-NLS-1$
     93                 + (INCLUDE_LDPI ? "|ldpi" : "") + ")$");  //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
     94 
     95     /** Pattern for version qualifiers */
     96     private final static Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$");//$NON-NLS-1$
     97 
     98     private static final String[] REQUIRED_DENSITIES = INCLUDE_LDPI
     99             ? new String[] { DRAWABLE_LDPI, DRAWABLE_MDPI, DRAWABLE_HDPI, DRAWABLE_XHDPI }
    100             : new String[] { DRAWABLE_MDPI, DRAWABLE_HDPI, DRAWABLE_XHDPI };
    101 
    102     private static final String[] DENSITY_QUALIFIERS =
    103         new String[] {
    104             "-ldpi",  //$NON-NLS-1$
    105             "-mdpi",  //$NON-NLS-1$
    106             "-hdpi",  //$NON-NLS-1$
    107             "-xhdpi"  //$NON-NLS-1$
    108     };
    109 
    110     /** Wrong icon size according to published conventions */
    111     public static final Issue ICON_EXPECTED_SIZE = Issue.create(
    112             "IconExpectedSize", //$NON-NLS-1$
    113             "Ensures that launcher icons, notification icons etc have the correct size",
    114             "There are predefined sizes (for each density) for launcher icons. You " +
    115             "should follow these conventions to make sure your icons fit in with the " +
    116             "overall look of the platform.",
    117             Category.ICONS,
    118             5,
    119             Severity.WARNING,
    120             IconDetector.class,
    121             Scope.ALL_RESOURCES_SCOPE)
    122             // Still some potential false positives:
    123             .setEnabledByDefault(false)
    124             .setMoreInfo(
    125             "http://developer.android.com/design/style/iconography.html"); //$NON-NLS-1$
    126 
    127     /** Inconsistent dip size across densities */
    128     public static final Issue ICON_DIP_SIZE = Issue.create(
    129             "IconDipSize", //$NON-NLS-1$
    130             "Ensures that icons across densities provide roughly the same density-independent size",
    131             "Checks the all icons which are provided in multiple densities, all compute to " +
    132             "roughly the same density-independent pixel (dip) size. This catches errors where " +
    133             "images are either placed in the wrong folder, or icons are changed to new sizes " +
    134             "but some folders are forgotten.",
    135             Category.ICONS,
    136             5,
    137             Severity.WARNING,
    138             IconDetector.class,
    139             Scope.ALL_RESOURCES_SCOPE);
    140 
    141     /** Images in res/drawable folder */
    142     public static final Issue ICON_LOCATION = Issue.create(
    143             "IconLocation", //$NON-NLS-1$
    144             "Ensures that images are not defined in the density-independent drawable folder",
    145             "The res/drawable folder is intended for density-independent graphics such as " +
    146             "shapes defined in XML. For bitmaps, move it to drawable-mdpi and consider " +
    147             "providing higher and lower resolution versions in drawable-ldpi, drawable-hdpi " +
    148             "and drawable-xhdpi. If the icon *really* is density independent (for example " +
    149             "a solid color) you can place it in drawable-nodpi.",
    150             Category.ICONS,
    151             5,
    152             Severity.WARNING,
    153             IconDetector.class,
    154             Scope.ALL_RESOURCES_SCOPE).setMoreInfo(
    155             "http://developer.android.com/guide/practices/screens_support.html"); //$NON-NLS-1$
    156 
    157     /** Missing density versions of image */
    158     public static final Issue ICON_DENSITIES = Issue.create(
    159             "IconDensities", //$NON-NLS-1$
    160             "Ensures that icons provide custom versions for all supported densities",
    161             "Icons will look best if a custom version is provided for each of the " +
    162             "major screen density classes (low, medium, high, extra high). " +
    163             "This lint check identifies icons which do not have complete coverage " +
    164             "across the densities.\n" +
    165             "\n" +
    166             "Low density is not really used much anymore, so this check ignores " +
    167             "the ldpi density. To force lint to include it, set the environment " +
    168             "variable ANDROID_LINT_INCLUDE_LDPI=true. For more information on " +
    169             "current density usage, see " +
    170             "http://developer.android.com/resources/dashboard/screens.html",
    171             Category.ICONS,
    172             4,
    173             Severity.WARNING,
    174             IconDetector.class,
    175             Scope.ALL_RESOURCES_SCOPE).setMoreInfo(
    176             "http://developer.android.com/guide/practices/screens_support.html"); //$NON-NLS-1$
    177 
    178     /** Missing density folders */
    179     public static final Issue ICON_MISSING_FOLDER = Issue.create(
    180             "IconMissingDensityFolder", //$NON-NLS-1$
    181             "Ensures that all the density folders are present",
    182             "Icons will look best if a custom version is provided for each of the " +
    183             "major screen density classes (low, medium, high, extra high). " +
    184             "This lint check identifies folders which are missing, such as drawable-hdpi." +
    185             "\n" +
    186             "Low density is not really used much anymore, so this check ignores " +
    187             "the ldpi density. To force lint to include it, set the environment " +
    188             "variable ANDROID_LINT_INCLUDE_LDPI=true. For more information on " +
    189             "current density usage, see " +
    190             "http://developer.android.com/resources/dashboard/screens.html",
    191             Category.ICONS,
    192             3,
    193             Severity.WARNING,
    194             IconDetector.class,
    195             Scope.ALL_RESOURCES_SCOPE).setMoreInfo(
    196             "http://developer.android.com/guide/practices/screens_support.html"); //$NON-NLS-1$
    197 
    198     /** Using .gif bitmaps */
    199     public static final Issue GIF_USAGE = Issue.create(
    200             "GifUsage", //$NON-NLS-1$
    201             "Checks for images using the GIF file format which is discouraged",
    202             "The .gif file format is discouraged. Consider using .png (preferred) " +
    203             "or .jpg (acceptable) instead.",
    204             Category.ICONS,
    205             5,
    206             Severity.WARNING,
    207             IconDetector.class,
    208             Scope.ALL_RESOURCES_SCOPE).setMoreInfo(
    209             "http://developer.android.com/guide/topics/resources/drawable-resource.html#Bitmap"); //$NON-NLS-1$
    210 
    211     /** Duplicated icons across different names */
    212     public static final Issue DUPLICATES_NAMES = Issue.create(
    213             "IconDuplicates", //$NON-NLS-1$
    214             "Finds duplicated icons under different names",
    215             "If an icon is repeated under different names, you can consolidate and just " +
    216             "use one of the icons and delete the others to make your application smaller. " +
    217             "However, duplicated icons usually are not intentional and can sometimes point " +
    218             "to icons that were accidentally overwritten or accidentally not updated.",
    219             Category.ICONS,
    220             3,
    221             Severity.WARNING,
    222             IconDetector.class,
    223             Scope.ALL_RESOURCES_SCOPE);
    224 
    225     /** Duplicated contents across configurations for a given name */
    226     public static final Issue DUPLICATES_CONFIGURATIONS = Issue.create(
    227             "IconDuplicatesConfig", //$NON-NLS-1$
    228             "Finds icons that have identical bitmaps across various configuration parameters",
    229             "If an icon is provided under different configuration parameters such as " +
    230             "drawable-hdpi or -v11, they should typically be different. This detector " +
    231             "catches cases where the same icon is provided in different configuration folder " +
    232             "which is usually not intentional.",
    233             Category.ICONS,
    234             5,
    235             Severity.WARNING,
    236             IconDetector.class,
    237             Scope.ALL_RESOURCES_SCOPE);
    238 
    239     /** Icons appearing in both -nodpi and a -Ndpi folder */
    240     public static final Issue ICON_NODPI = Issue.create(
    241             "IconNoDpi", //$NON-NLS-1$
    242             "Finds icons that appear in both a -nodpi folder and a dpi folder",
    243             "Bitmaps that appear in drawable-nodpi folders will not be scaled by the " +
    244             "Android framework. If a drawable resource of the same name appears *both* in " +
    245             "a -nodpi folder as well as a dpi folder such as drawable-hdpi, then " +
    246             "the behavior is ambiguous and probably not intentional. Delete one or the " +
    247             "other, or use different names for the icons.",
    248             Category.ICONS,
    249             7,
    250             Severity.WARNING,
    251             IconDetector.class,
    252             Scope.ALL_RESOURCES_SCOPE);
    253 
    254     private String mApplicationIcon;
    255 
    256     /** Constructs a new {@link IconDetector} check */
    257     public IconDetector() {
    258     }
    259 
    260     @Override
    261     public Speed getSpeed() {
    262         return Speed.SLOW;
    263     }
    264 
    265     @Override
    266     public void beforeCheckProject(Context context) {
    267         mApplicationIcon = null;
    268     }
    269 
    270     @Override
    271     public void afterCheckLibraryProject(Context context) {
    272         checkResourceFolder(context, context.getProject().getDir());
    273     }
    274 
    275     @Override
    276     public void afterCheckProject(Context context) {
    277         checkResourceFolder(context, context.getProject().getDir());
    278     }
    279 
    280     private void checkResourceFolder(Context context, File dir) {
    281         File res = new File(dir, RES_FOLDER);
    282         if (res.isDirectory()) {
    283             File[] folders = res.listFiles();
    284             if (folders != null) {
    285                 boolean checkFolders = context.isEnabled(ICON_DENSITIES)
    286                         || context.isEnabled(ICON_MISSING_FOLDER)
    287                         || context.isEnabled(ICON_NODPI);
    288                 boolean checkDipSizes = context.isEnabled(ICON_DIP_SIZE);
    289                 boolean checkDuplicates = context.isEnabled(DUPLICATES_NAMES)
    290                          || context.isEnabled(DUPLICATES_CONFIGURATIONS);
    291 
    292                 Map<File, Dimension> pixelSizes = null;
    293                 Map<File, Long> fileSizes = null;
    294                 if (checkDipSizes || checkDuplicates) {
    295                     pixelSizes = new HashMap<File, Dimension>();
    296                     fileSizes = new HashMap<File, Long>();
    297                 }
    298                 Map<File, Set<String>> folderToNames = new HashMap<File, Set<String>>();
    299                 for (File folder : folders) {
    300                     String folderName = folder.getName();
    301                     if (folderName.startsWith(DRAWABLE_FOLDER)) {
    302                         File[] files = folder.listFiles();
    303                         if (files != null) {
    304                             checkDrawableDir(context, folder, files, pixelSizes, fileSizes);
    305 
    306                             if (checkFolders && DENSITY_PATTERN.matcher(folderName).matches()) {
    307                                 Set<String> names = new HashSet<String>(files.length);
    308                                 for (File f : files) {
    309                                     String name = f.getName();
    310                                     if (isDrawableFile(name)) {
    311                                         names.add(f.getName());
    312                                     }
    313                                 }
    314                                 folderToNames.put(folder, names);
    315                             }
    316                         }
    317                     }
    318                 }
    319 
    320                 if (checkDipSizes) {
    321                     checkDipSizes(context, pixelSizes);
    322                 }
    323 
    324                 if (checkDuplicates) {
    325                     checkDuplicates(context, pixelSizes, fileSizes);
    326                 }
    327 
    328                 if (checkFolders && folderToNames.size() > 0) {
    329                     checkDensities(context, res, folderToNames);
    330                 }
    331             }
    332         }
    333     }
    334 
    335     private static boolean isDrawableFile(String name) {
    336         // endsWith(name, DOT_PNG) is also true for endsWith(name, DOT_9PNG)
    337         return endsWith(name, DOT_PNG)|| endsWith(name, DOT_JPG) || endsWith(name, DOT_GIF) ||
    338                 endsWith(name, DOT_XML);
    339     }
    340 
    341     // This method looks for duplicates in the assets. This uses two pieces of information
    342     // (file sizes and image dimensions) to quickly reject candidates, such that it only
    343     // needs to check actual file contents on a small subset of the available files.
    344     private void checkDuplicates(Context context, Map<File, Dimension> pixelSizes,
    345             Map<File, Long> fileSizes) {
    346         Map<Long, Set<File>> sameSizes = new HashMap<Long, Set<File>>();
    347         Map<Long, File> seenSizes = new HashMap<Long, File>(fileSizes.size());
    348         for (Map.Entry<File, Long> entry : fileSizes.entrySet()) {
    349             File file = entry.getKey();
    350             Long size = entry.getValue();
    351             if (seenSizes.containsKey(size)) {
    352                 Set<File> set = sameSizes.get(size);
    353                 if (set == null) {
    354                     set = new HashSet<File>();
    355                     set.add(seenSizes.get(size));
    356                     sameSizes.put(size, set);
    357                 }
    358                 set.add(file);
    359             } else {
    360                 seenSizes.put(size, file);
    361             }
    362         }
    363 
    364         if (sameSizes.size() == 0) {
    365             return;
    366         }
    367 
    368         // Now go through the files that have the same size and check to see if we can
    369         // split them apart based on image dimensions
    370         // Note: we may not have file sizes on all the icons; in particular,
    371         // we don't have file sizes for ninepatch files.
    372         Collection<Set<File>> candidateLists = sameSizes.values();
    373         for (Set<File> candidates : candidateLists) {
    374             Map<Dimension, Set<File>> sameDimensions = new HashMap<Dimension, Set<File>>(
    375                     candidates.size());
    376             List<File> noSize = new ArrayList<File>();
    377             for (File file : candidates) {
    378                 Dimension dimension = pixelSizes.get(file);
    379                 if (dimension != null) {
    380                     Set<File> set = sameDimensions.get(dimension);
    381                     if (set == null) {
    382                         set = new HashSet<File>();
    383                         sameDimensions.put(dimension, set);
    384                     }
    385                     set.add(file);
    386                 } else {
    387                     noSize.add(file);
    388                 }
    389             }
    390 
    391 
    392             // Files that we have no dimensions for must be compared against everything
    393             Collection<Set<File>> sets = sameDimensions.values();
    394             if (noSize.size() > 0) {
    395                 if (sets.size() > 0) {
    396                     for (Set<File> set : sets) {
    397                         set.addAll(noSize);
    398                     }
    399                 } else {
    400                     // Must just test the noSize elements against themselves
    401                     HashSet<File> noSizeSet = new HashSet<File>(noSize);
    402                     sets = Collections.<Set<File>>singletonList(noSizeSet);
    403                 }
    404             }
    405 
    406             // Map from file to actual byte contents of the file.
    407             // We store this in a map such that for repeated files, such as noSize files
    408             // which can appear in multiple buckets, we only need to read them once
    409             Map<File, byte[]> fileContents = new HashMap<File, byte[]>();
    410 
    411             // Now we're ready for the final check where we actually check the
    412             // bits. We have to partition the files into buckets of files that
    413             // are identical.
    414             for (Set<File> set : sets) {
    415                 if (set.size() < 2) {
    416                     continue;
    417                 }
    418 
    419                 // Read all files in this set and store in map
    420                 for (File file : set) {
    421                     byte[] bits = fileContents.get(file);
    422                     if (bits == null) {
    423                         try {
    424                             bits = Files.toByteArray(file);
    425                             fileContents.put(file, bits);
    426                         } catch (IOException e) {
    427                             context.log(e, null);
    428                         }
    429                     }
    430                 }
    431 
    432                 // Map where the key file is known to be equal to the value file.
    433                 // After we check individual files for equality this will be used
    434                 // to look for transitive equality.
    435                 Map<File, File> equal = new HashMap<File, File>();
    436 
    437                 // Now go and compare all the files. This isn't an efficient algorithm
    438                 // but the number of candidates should be very small
    439 
    440                 List<File> files = new ArrayList<File>(set);
    441                 Collections.sort(files);
    442                 for (int i = 0; i < files.size() - 1; i++) {
    443                     for (int j = i + 1; j < files.size(); j++) {
    444                         File file1 = files.get(i);
    445                         File file2 = files.get(j);
    446                         byte[] contents1 = fileContents.get(file1);
    447                         byte[] contents2 = fileContents.get(file2);
    448                         if (contents1 == null || contents2 == null) {
    449                             // File couldn't be read: ignore
    450                             continue;
    451                         }
    452                         if (contents1.length != contents2.length) {
    453                             // Sizes differ: not identical.
    454                             // This shouldn't happen since we've already partitioned based
    455                             // on File.length(), but just make sure here since the file
    456                             // system could have lied, or cached a value that has changed
    457                             // if the file was just overwritten
    458                             continue;
    459                         }
    460                         boolean same = true;
    461                         for (int k = 0; k < contents1.length; k++) {
    462                             if (contents1[k] != contents2[k]) {
    463                                 same = false;
    464                                 break;
    465                             }
    466                         }
    467                         if (same) {
    468                             equal.put(file1, file2);
    469                         }
    470                     }
    471                 }
    472 
    473                 if (equal.size() > 0) {
    474                     Map<File, Set<File>> partitions = new HashMap<File, Set<File>>();
    475                     List<Set<File>> sameSets = new ArrayList<Set<File>>();
    476                     for (Map.Entry<File, File> entry : equal.entrySet()) {
    477                         File file1 = entry.getKey();
    478                         File file2 = entry.getValue();
    479                         Set<File> set1 = partitions.get(file1);
    480                         Set<File> set2 = partitions.get(file2);
    481                         if (set1 != null) {
    482                             set1.add(file2);
    483                         } else if (set2 != null) {
    484                             set2.add(file1);
    485                         } else {
    486                             set = new HashSet<File>();
    487                             sameSets.add(set);
    488                             set.add(file1);
    489                             set.add(file2);
    490                             partitions.put(file1, set);
    491                             partitions.put(file2, set);
    492                         }
    493                     }
    494 
    495                     // We've computed the partitions of equal files. Now sort them
    496                     // for stable output.
    497                     List<List<File>> lists = new ArrayList<List<File>>();
    498                     for (Set<File> same : sameSets) {
    499                         assert same.size() > 0;
    500                         ArrayList<File> sorted = new ArrayList<File>(same);
    501                         Collections.sort(sorted);
    502                         lists.add(sorted);
    503                     }
    504                     // Sort overall partitions by the first item in each list
    505                     Collections.sort(lists, new Comparator<List<File>>() {
    506                         @Override
    507                         public int compare(List<File> list1, List<File> list2) {
    508                             return list1.get(0).compareTo(list2.get(0));
    509                         }
    510                     });
    511 
    512                     for (List<File> sameFiles : lists) {
    513                         Location location = null;
    514                         boolean sameNames = true;
    515                         String lastName = null;
    516                         for (File file : sameFiles) {
    517                              if (lastName != null && !lastName.equals(file.getName())) {
    518                                 sameNames = false;
    519                             }
    520                             lastName = file.getName();
    521                             // Chain locations together
    522                             Location linkedLocation = location;
    523                             location = Location.create(file);
    524                             location.setSecondary(linkedLocation);
    525                         }
    526 
    527                         if (sameNames) {
    528                             StringBuilder sb = new StringBuilder();
    529                             for (File file : sameFiles) {
    530                                 if (sb.length() > 0) {
    531                                     sb.append(", "); //$NON-NLS-1$
    532                                 }
    533                                 sb.append(file.getParentFile().getName());
    534                             }
    535                             String message = String.format(
    536                                 "The %1$s icon has identical contents in the following configuration folders: %2$s",
    537                                         lastName, sb.toString());
    538                                 context.report(DUPLICATES_CONFIGURATIONS, location, message, null);
    539                         } else {
    540                             StringBuilder sb = new StringBuilder();
    541                             for (File file : sameFiles) {
    542                                 if (sb.length() > 0) {
    543                                     sb.append(", "); //$NON-NLS-1$
    544                                 }
    545                                 sb.append(file.getName());
    546                             }
    547                             String message = String.format(
    548                                 "The following unrelated icon files have identical contents: %1$s",
    549                                         sb.toString());
    550                                 context.report(DUPLICATES_NAMES, location, message, null);
    551                         }
    552                     }
    553                 }
    554             }
    555         }
    556 
    557     }
    558 
    559     // This method checks the given map from resource file to pixel dimensions for each
    560     // such image and makes sure that the normalized dip sizes across all the densities
    561     // are mostly the same.
    562     private void checkDipSizes(Context context, Map<File, Dimension> pixelSizes) {
    563         // Partition up the files such that I can look at a series by name. This
    564         // creates a map from filename (such as foo.png) to a list of files
    565         // providing that icon in various folders: drawable-mdpi/foo.png, drawable-hdpi/foo.png
    566         // etc.
    567         Map<String, List<File>> nameToFiles = new HashMap<String, List<File>>();
    568         for (File file : pixelSizes.keySet()) {
    569             String name = file.getName();
    570             List<File> list = nameToFiles.get(name);
    571             if (list == null) {
    572                 list = new ArrayList<File>();
    573                 nameToFiles.put(name, list);
    574             }
    575             list.add(file);
    576         }
    577 
    578         ArrayList<String> names = new ArrayList<String>(nameToFiles.keySet());
    579         Collections.sort(names);
    580 
    581         // We have to partition the files further because it's possible for the project
    582         // to have different configurations for an icon, such as this:
    583         //   drawable-large-hdpi/foo.png, drawable-large-mdpi/foo.png,
    584         //   drawable-hdpi/foo.png, drawable-mdpi/foo.png,
    585         //    drawable-hdpi-v11/foo.png and drawable-mdpi-v11/foo.png.
    586         // In this case we don't want to compare across categories; we want to
    587         // ensure that the drawable-large-{density} icons are consistent,
    588         // that the drawable-{density}-v11 icons are consistent, and that
    589         // the drawable-{density} icons are consistent.
    590 
    591         // Map from name to list of map from parent folder to list of files
    592         Map<String, Map<String, List<File>>> configMap =
    593                 new HashMap<String, Map<String,List<File>>>();
    594         for (Map.Entry<String, List<File>> entry : nameToFiles.entrySet()) {
    595             String name = entry.getKey();
    596             List<File> files = entry.getValue();
    597             for (File file : files) {
    598                 String parentName = file.getParentFile().getName();
    599                 // Strip out the density part
    600                 int index = -1;
    601                 for (String qualifier : DENSITY_QUALIFIERS) {
    602                     index = parentName.indexOf(qualifier);
    603                     if (index != -1) {
    604                         parentName = parentName.substring(0, index)
    605                                 + parentName.substring(index + qualifier.length());
    606                         break;
    607                     }
    608                 }
    609                 if (index == -1) {
    610                     // No relevant qualifier found in the parent directory name,
    611                     // e.g. it's just "drawable" or something like "drawable-nodpi".
    612                     continue;
    613                 }
    614 
    615                 Map<String, List<File>> folderMap = configMap.get(name);
    616                 if (folderMap == null) {
    617                     folderMap = new HashMap<String,List<File>>();
    618                     configMap.put(name, folderMap);
    619                 }
    620                 // Map from name to a map from parent folder to files
    621                 List<File> list = folderMap.get(parentName);
    622                 if (list == null) {
    623                     list = new ArrayList<File>();
    624                     folderMap.put(parentName, list);
    625                 }
    626                 list.add(file);
    627             }
    628         }
    629 
    630         for (String name : names) {
    631             //List<File> files = nameToFiles.get(name);
    632             Map<String, List<File>> configurations = configMap.get(name);
    633             if (configurations == null) {
    634                 // Nothing in this configuration: probably only found in drawable/ or
    635                 // drawable-nodpi etc directories.
    636                 continue;
    637             }
    638 
    639             for (Map.Entry<String, List<File>> entry : configurations.entrySet()) {
    640                 List<File> files = entry.getValue();
    641 
    642                 // Ensure that all the dip sizes are *roughly* the same
    643                 Map<File, Dimension> dipSizes = new HashMap<File, Dimension>();
    644                 int dipWidthSum = 0; // Incremental computation of average
    645                 int dipHeightSum = 0; // Incremental computation of average
    646                 int count = 0;
    647                 for (File file : files) {
    648                     float factor = getMdpiScalingFactor(file.getParentFile().getName());
    649                     if (factor > 0) {
    650                         Dimension size = pixelSizes.get(file);
    651                         Dimension dip = new Dimension(
    652                                 Math.round(size.width / factor),
    653                                 Math.round(size.height / factor));
    654                         dipWidthSum += dip.width;
    655                         dipHeightSum += dip.height;
    656                         dipSizes.put(file, dip);
    657                         count++;
    658                     }
    659                 }
    660                 if (count == 0) {
    661                     // Icons in drawable/ and drawable-nodpi/
    662                     continue;
    663                 }
    664                 int meanWidth = dipWidthSum / count;
    665                 int meanHeight = dipHeightSum / count;
    666 
    667                 // Compute standard deviation?
    668                 int squareWidthSum = 0;
    669                 int squareHeightSum = 0;
    670                 for (Dimension size : dipSizes.values()) {
    671                     squareWidthSum += (size.width - meanWidth) * (size.width - meanWidth);
    672                     squareHeightSum += (size.height - meanHeight) * (size.height - meanHeight);
    673                 }
    674                 double widthStdDev = Math.sqrt(squareWidthSum / count);
    675                 double heightStdDev = Math.sqrt(squareHeightSum / count);
    676 
    677                 if (widthStdDev > meanWidth / 10 || heightStdDev > meanHeight) {
    678                     Location location = null;
    679                     StringBuilder sb = new StringBuilder();
    680 
    681                     // Sort entries by decreasing dip size
    682                     List<Map.Entry<File, Dimension>> entries =
    683                             new ArrayList<Map.Entry<File,Dimension>>();
    684                     for (Map.Entry<File, Dimension> entry2 : dipSizes.entrySet()) {
    685                         entries.add(entry2);
    686                     }
    687                     Collections.sort(entries,
    688                             new Comparator<Map.Entry<File, Dimension>>() {
    689                         @Override
    690                         public int compare(Entry<File, Dimension> e1,
    691                                 Entry<File, Dimension> e2) {
    692                             Dimension d1 = e1.getValue();
    693                             Dimension d2 = e2.getValue();
    694                             if (d1.width != d2.width) {
    695                                 return d2.width - d1.width;
    696                             }
    697 
    698                             return d2.height - d1.height;
    699                         }
    700                     });
    701                     for (Map.Entry<File, Dimension> entry2 : entries) {
    702                         if (sb.length() > 0) {
    703                             sb.append(", ");
    704                         }
    705                         File file = entry2.getKey();
    706 
    707                         // Chain locations together
    708                         Location linkedLocation = location;
    709                         location = Location.create(file);
    710                         location.setSecondary(linkedLocation);
    711                         Dimension dip = entry2.getValue();
    712                         Dimension px = pixelSizes.get(file);
    713                         String fileName = file.getParentFile().getName() + File.separator
    714                                 + file.getName();
    715                         sb.append(String.format("%1$s: %2$dx%3$d dp (%4$dx%5$d px)",
    716                                 fileName, dip.width, dip.height, px.width, px.height));
    717                     }
    718                     String message = String.format(
    719                         "The image %1$s varies significantly in its density-independent (dip) " +
    720                         "size across the various density versions: %2$s",
    721                             name, sb.toString());
    722                     context.report(ICON_DIP_SIZE, location, message, null);
    723                 }
    724             }
    725         }
    726     }
    727 
    728     private void checkDensities(Context context, File res, Map<File, Set<String>> folderToNames) {
    729         // TODO: Is there a way to look at the manifest and figure out whether
    730         // all densities are expected to be needed?
    731         // Note: ldpi is probably not needed; it has very little usage
    732         // (about 2%; http://developer.android.com/resources/dashboard/screens.html)
    733         // TODO: Use the matrix to check out if we can eliminate densities based
    734         // on the target screens?
    735 
    736         Set<String> definedDensities = new HashSet<String>();
    737         for (File f : folderToNames.keySet()) {
    738             definedDensities.add(f.getName());
    739         }
    740 
    741         // Look for missing folders -- if you define say drawable-mdpi then you
    742         // should also define -hdpi and -xhdpi.
    743         if (context.isEnabled(ICON_MISSING_FOLDER)) {
    744             List<String> missing = new ArrayList<String>();
    745             for (String density : REQUIRED_DENSITIES) {
    746                 if (!definedDensities.contains(density)) {
    747                     missing.add(density);
    748                 }
    749             }
    750             if (missing.size() > 0 ) {
    751                 context.report(
    752                     ICON_MISSING_FOLDER,
    753                     Location.create(res),
    754                     String.format("Missing density variation folders in %1$s: %2$s",
    755                             context.getProject().getDisplayPath(res),
    756                             LintUtils.formatList(missing, -1)),
    757                     null);
    758             }
    759         }
    760 
    761         if (context.isEnabled(ICON_NODPI)) {
    762             Set<String> noDpiNames = new HashSet<String>();
    763             for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) {
    764                 if (isNoDpiFolder(entry.getKey())) {
    765                     noDpiNames.addAll(entry.getValue());
    766                 }
    767             }
    768             if (noDpiNames.size() > 0) {
    769                 // Make sure that none of the nodpi names appear in a non-nodpi folder
    770                 Set<String> inBoth = new HashSet<String>();
    771                 List<File> files = new ArrayList<File>();
    772                 for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) {
    773                     File folder = entry.getKey();
    774                     String folderName = folder.getName();
    775                     if (!isNoDpiFolder(folder)) {
    776                         assert DENSITY_PATTERN.matcher(folderName).matches();
    777                         Set<String> overlap = nameIntersection(noDpiNames, entry.getValue());
    778                         inBoth.addAll(overlap);
    779                         for (String name : overlap) {
    780                             files.add(new File(folder, name));
    781                         }
    782                     }
    783                 }
    784 
    785                 if (inBoth.size() > 0) {
    786                     List<String> list = new ArrayList<String>(inBoth);
    787                     Collections.sort(list);
    788 
    789                     // Chain locations together
    790                     Collections.sort(files);
    791                     Location location = null;
    792                     for (File file : files) {
    793                         Location linkedLocation = location;
    794                         location = Location.create(file);
    795                         location.setSecondary(linkedLocation);
    796                     }
    797 
    798                     context.report(ICON_NODPI, location,
    799                         String.format(
    800                             "The following images appear in both -nodpi and in a density folder: %1$s",
    801                             LintUtils.formatList(list, 10)),
    802                         null);
    803                 }
    804             }
    805         }
    806 
    807         if (context.isEnabled(ICON_DENSITIES)) {
    808             // Look for folders missing some of the specific assets
    809             Set<String> allNames = new HashSet<String>();
    810             for (Entry<File,Set<String>> entry : folderToNames.entrySet()) {
    811                 if (!isNoDpiFolder(entry.getKey())) {
    812                     Set<String> names = entry.getValue();
    813                     allNames.addAll(names);
    814                 }
    815             }
    816 
    817             for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) {
    818                 File file = entry.getKey();
    819                 if (isNoDpiFolder(file)) {
    820                     continue;
    821                 }
    822                 Set<String> names = entry.getValue();
    823                 if (names.size() != allNames.size()) {
    824                     List<String> delta = new ArrayList<String>(nameDifferences(allNames,  names));
    825                     if (delta.size() == 0) {
    826                         continue;
    827                     }
    828                     Collections.sort(delta);
    829                     String foundIn = "";
    830                     if (delta.size() == 1) {
    831                         // Produce list of where the icon is actually defined
    832                         List<String> defined = new ArrayList<String>();
    833                         String name = delta.get(0);
    834                         for (Map.Entry<File, Set<String>> e : folderToNames.entrySet()) {
    835                             if (e.getValue().contains(name)) {
    836                                 defined.add(e.getKey().getName());
    837                             }
    838                         }
    839                         if (defined.size() > 0) {
    840                             foundIn = String.format(" (found in %1$s)",
    841                                     LintUtils.formatList(defined, 5));
    842                         }
    843                     }
    844 
    845                     context.report(ICON_DENSITIES, Location.create(file),
    846                             String.format(
    847                                     "Missing the following drawables in %1$s: %2$s%3$s",
    848                                     file.getName(),
    849                                     LintUtils.formatList(delta, 5),
    850                                     foundIn),
    851                             null);
    852                 }
    853             }
    854         }
    855     }
    856 
    857     /**
    858      * Compute the difference in names between a and b. This is not just
    859      * Sets.difference(a, b) because we want to make the comparisons <b>without
    860      * file extensions</b> and return the result <b>with</b>..
    861      */
    862     private Set<String> nameDifferences(Set<String> a, Set<String> b) {
    863         Set<String> names1 = new HashSet<String>(a.size());
    864         for (String s : a) {
    865             names1.add(LintUtils.getBaseName(s));
    866         }
    867         Set<String> names2 = new HashSet<String>(b.size());
    868         for (String s : b) {
    869             names2.add(LintUtils.getBaseName(s));
    870         }
    871 
    872         names1.removeAll(names2);
    873 
    874         if (names1.size() > 0) {
    875             // Map filenames back to original filenames with extensions
    876             Set<String> result = new HashSet<String>(names1.size());
    877             for (String s : a) {
    878                 if (names1.contains(LintUtils.getBaseName(s))) {
    879                     result.add(s);
    880                 }
    881             }
    882             for (String s : b) {
    883                 if (names1.contains(LintUtils.getBaseName(s))) {
    884                     result.add(s);
    885                 }
    886             }
    887 
    888             return result;
    889         }
    890 
    891         return Collections.emptySet();
    892     }
    893 
    894     /**
    895      * Compute the intersection in names between a and b. This is not just
    896      * Sets.intersection(a, b) because we want to make the comparisons <b>without
    897      * file extensions</b> and return the result <b>with</b>.
    898      */
    899     private Set<String> nameIntersection(Set<String> a, Set<String> b) {
    900         Set<String> names1 = new HashSet<String>(a.size());
    901         for (String s : a) {
    902             names1.add(LintUtils.getBaseName(s));
    903         }
    904         Set<String> names2 = new HashSet<String>(b.size());
    905         for (String s : b) {
    906             names2.add(LintUtils.getBaseName(s));
    907         }
    908 
    909         names1.retainAll(names2);
    910 
    911         if (names1.size() > 0) {
    912             // Map filenames back to original filenames with extensions
    913             Set<String> result = new HashSet<String>(names1.size());
    914             for (String s : a) {
    915                 if (names1.contains(LintUtils.getBaseName(s))) {
    916                     result.add(s);
    917                 }
    918             }
    919             for (String s : b) {
    920                 if (names1.contains(LintUtils.getBaseName(s))) {
    921                     result.add(s);
    922                 }
    923             }
    924 
    925             return result;
    926         }
    927 
    928         return Collections.emptySet();
    929     }
    930 
    931     private static boolean isNoDpiFolder(File file) {
    932         return file.getName().contains("-nodpi");
    933     }
    934 
    935     private void checkDrawableDir(Context context, File folder, File[] files,
    936             Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes) {
    937         if (folder.getName().equals(DRAWABLE_FOLDER)
    938                 && context.isEnabled(ICON_LOCATION) &&
    939                 // If supporting older versions than Android 1.6, it's not an error
    940                 // to include bitmaps in drawable/
    941                 context.getProject().getMinSdk() >= 4) {
    942             for (File file : files) {
    943                 String name = file.getName();
    944                 if (name.endsWith(DOT_XML)) {
    945                     // pass - most common case, avoids checking other extensions
    946                 } else if (endsWith(name, DOT_PNG)
    947                         || endsWith(name, DOT_JPG)
    948                         || endsWith(name, DOT_GIF)) {
    949                     context.report(ICON_LOCATION,
    950                         Location.create(file),
    951                         String.format("Found bitmap drawable res/drawable/%1$s in " +
    952                                 "densityless folder",
    953                                 file.getName()),
    954                         null);
    955                 }
    956             }
    957         }
    958 
    959         if (context.isEnabled(GIF_USAGE)) {
    960             for (File file : files) {
    961                 String name = file.getName();
    962                 if (endsWith(name, DOT_GIF)) {
    963                     context.report(GIF_USAGE, Location.create(file),
    964                             "Using the .gif format for bitmaps is discouraged",
    965                             null);
    966                 }
    967             }
    968         }
    969 
    970         // Check icon sizes
    971         if (context.isEnabled(ICON_EXPECTED_SIZE)) {
    972             checkExpectedSizes(context, folder, files);
    973         }
    974 
    975         if (pixelSizes != null || fileSizes != null) {
    976             for (File file : files) {
    977                 // TODO: Combine this check with the check for expected sizes such that
    978                 // I don't check file sizes twice!
    979                 String fileName = file.getName();
    980 
    981                 if (endsWith(fileName, DOT_PNG) || endsWith(fileName, DOT_JPG)) {
    982                     // Only scan .png files (except 9-patch png's) and jpg files for
    983                     // dip sizes. Duplicate checks can also be performed on ninepatch files.
    984                     if (pixelSizes != null && !endsWith(fileName, DOT_9PNG)) {
    985                         Dimension size = getSize(file);
    986                         pixelSizes.put(file, size);
    987                     }
    988                     if (fileSizes != null) {
    989                         fileSizes.put(file, file.length());
    990                     }
    991                 }
    992             }
    993         }
    994     }
    995 
    996     private void checkExpectedSizes(Context context, File folder, File[] files) {
    997         String folderName = folder.getName();
    998 
    999         int folderVersion = -1;
   1000         String[] qualifiers = folderName.split("-"); //$NON-NLS-1$
   1001         for (String qualifier : qualifiers) {
   1002             if (qualifier.startsWith("v")) {
   1003                 Matcher matcher = VERSION_PATTERN.matcher(qualifier);
   1004                 if (matcher.matches()) {
   1005                     folderVersion = Integer.parseInt(matcher.group(1));
   1006                 }
   1007             }
   1008         }
   1009 
   1010         for (File file : files) {
   1011             String name = file.getName();
   1012 
   1013             // TODO: Look up exact app icon from the manifest rather than simply relying on
   1014             // the naming conventions described here:
   1015             //  http://developer.android.com/guide/practices/ui_guidelines/icon_design.html#design-tips
   1016             // See if we can figure out other types of icons from usage too.
   1017 
   1018             String baseName = name;
   1019             int index = baseName.indexOf('.');
   1020             if (index != -1) {
   1021                 baseName = baseName.substring(0, index);
   1022             }
   1023 
   1024             if (baseName.equals(mApplicationIcon) || name.startsWith("ic_launcher")) { //$NON-NLS-1$
   1025                 // Launcher icons
   1026                 checkSize(context, folderName, file, 48, 48, true /*exact*/);
   1027             } else if (name.startsWith("ic_action_")) { //$NON-NLS-1$
   1028                 // Action Bar
   1029                 checkSize(context, folderName, file, 32, 32, true /*exact*/);
   1030             } else if (name.startsWith("ic_dialog_")) { //$NON-NLS-1$
   1031                 // Dialog
   1032                 checkSize(context, folderName, file, 32, 32, true /*exact*/);
   1033             } else if (name.startsWith("ic_tab_")) { //$NON-NLS-1$
   1034                 // Tab icons
   1035                 checkSize(context, folderName, file, 32, 32, true /*exact*/);
   1036             } else if (name.startsWith("ic_stat_")) { //$NON-NLS-1$
   1037                 // Notification icons
   1038 
   1039                 if (isAndroid30(context, folderVersion)) {
   1040                     checkSize(context, folderName, file, 24, 24, true /*exact*/);
   1041                 } else if (isAndroid23(context, folderVersion)) {
   1042                     checkSize(context, folderName, file, 16, 25, false /*exact*/);
   1043                 } else {
   1044                     // Android 2.2 or earlier
   1045                     // TODO: Should this be done for each folder size?
   1046                     checkSize(context, folderName, file, 25, 25, true /*exact*/);
   1047                 }
   1048             } else if (name.startsWith("ic_menu_")) { //$NON-NLS-1$
   1049                 // Menu icons (<=2.3 only: Replaced by action bar icons (ic_action_ in 3.0).
   1050                 if (isAndroid23(context, folderVersion)) {
   1051                     // The icon should be 32x32 inside the transparent image; should
   1052                     // we check that this is mostly the case (a few pixels are allowed to
   1053                     // overlap for anti-aliasing etc)
   1054                     checkSize(context, folderName, file, 48, 48, true /*exact*/);
   1055                 } else {
   1056                     // Android 2.2 or earlier
   1057                     // TODO: Should this be done for each folder size?
   1058                     checkSize(context, folderName, file, 48, 48, true /*exact*/);
   1059                 }
   1060             }
   1061             // TODO: ListView icons?
   1062         }
   1063     }
   1064 
   1065     /**
   1066      * Is this drawable folder for an Android 3.0 drawable? This will be the
   1067      * case if it specifies -v11+, or if the minimum SDK version declared in the
   1068      * manifest is at least 11.
   1069      */
   1070     private boolean isAndroid30(Context context, int folderVersion) {
   1071         return folderVersion >= 11 || context.getMainProject().getMinSdk() >= 11;
   1072     }
   1073 
   1074     /**
   1075      * Is this drawable folder for an Android 2.3 drawable? This will be the
   1076      * case if it specifies -v9 or -v10, or if the minimum SDK version declared in the
   1077      * manifest is 9 or 10 (and it does not specify some higher version like -v11
   1078      */
   1079     private boolean isAndroid23(Context context, int folderVersion) {
   1080         if (isAndroid30(context, folderVersion)) {
   1081             return false;
   1082         }
   1083 
   1084         if (folderVersion == 9 || folderVersion == 10) {
   1085             return true;
   1086         }
   1087 
   1088         int minSdk = context.getMainProject().getMinSdk();
   1089 
   1090         return minSdk == 9 || minSdk == 10;
   1091     }
   1092 
   1093     private float getMdpiScalingFactor(String folderName) {
   1094         // Can't do startsWith(DRAWABLE_MDPI) because the folder could
   1095         // be something like "drawable-sw600dp-mdpi".
   1096         if (folderName.contains("-mdpi")) {            //$NON-NLS-1$
   1097             return 1.0f;
   1098         } else if (folderName.contains("-hdpi")) {     //$NON-NLS-1$
   1099             return 1.5f;
   1100         } else if (folderName.contains("-xhdpi")) {    //$NON-NLS-1$
   1101             return 2.0f;
   1102         } else if (folderName.contains("-ldpi")) {     //$NON-NLS-1$
   1103             return 0.75f;
   1104         } else {
   1105             return 0f;
   1106         }
   1107     }
   1108 
   1109     private void checkSize(Context context, String folderName, File file,
   1110             int mdpiWidth, int mdpiHeight, boolean exactMatch) {
   1111         String fileName = file.getName();
   1112         // Only scan .png files (except 9-patch png's) and jpg files
   1113         if (!((endsWith(fileName, DOT_PNG) && !endsWith(fileName, DOT_9PNG)) ||
   1114                 endsWith(fileName, DOT_JPG))) {
   1115             return;
   1116         }
   1117 
   1118         int width = -1;
   1119         int height = -1;
   1120         // Use 3:4:6:8 scaling ratio to look up the other expected sizes
   1121         if (folderName.startsWith(DRAWABLE_MDPI)) {
   1122             width = mdpiWidth;
   1123             height = mdpiHeight;
   1124         } else if (folderName.startsWith(DRAWABLE_HDPI)) {
   1125             // Perform math using floating point; if we just do
   1126             //   width = mdpiWidth * 3 / 2;
   1127             // then for mdpiWidth = 25 (as in notification icons on pre-GB) we end up
   1128             // with width = 37, instead of 38 (with floating point rounding we get 37.5 = 38)
   1129             width = Math.round(mdpiWidth * 3.f / 2);
   1130             height = Math.round(mdpiHeight * 3f / 2);
   1131         } else if (folderName.startsWith(DRAWABLE_XHDPI)) {
   1132             width = mdpiWidth * 2;
   1133             height = mdpiHeight * 2;
   1134         } else if (folderName.startsWith(DRAWABLE_LDPI)) {
   1135             width = Math.round(mdpiWidth * 3f / 4);
   1136             height = Math.round(mdpiHeight * 3f / 4);
   1137         } else {
   1138             return;
   1139         }
   1140 
   1141         Dimension size = getSize(file);
   1142         if (size != null) {
   1143             if (exactMatch && size.width != width || size.height != height) {
   1144                 context.report(
   1145                         ICON_EXPECTED_SIZE,
   1146                     Location.create(file),
   1147                     String.format(
   1148                         "Incorrect icon size for %1$s: expected %2$dx%3$d, but was %4$dx%5$d",
   1149                         folderName + File.separator + file.getName(),
   1150                         width, height, size.width, size.height),
   1151                     null);
   1152             } else if (!exactMatch && size.width > width || size.height > height) {
   1153                 context.report(
   1154                         ICON_EXPECTED_SIZE,
   1155                     Location.create(file),
   1156                     String.format(
   1157                         "Incorrect icon size for %1$s: icon size should be at most %2$dx%3$d, but was %4$dx%5$d",
   1158                         folderName + File.separator + file.getName(),
   1159                         width, height, size.width, size.height),
   1160                     null);
   1161             }
   1162         }
   1163     }
   1164 
   1165     private Dimension getSize(File file) {
   1166         try {
   1167             ImageInputStream input = ImageIO.createImageInputStream(file);
   1168             if (input != null) {
   1169                 try {
   1170                     Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
   1171                     if (readers.hasNext()) {
   1172                         ImageReader reader = readers.next();
   1173                         try {
   1174                             reader.setInput(input);
   1175                             return new Dimension(reader.getWidth(0), reader.getHeight(0));
   1176                         } finally {
   1177                             reader.dispose();
   1178                         }
   1179                     }
   1180                 } finally {
   1181                     if (input != null) {
   1182                         input.close();
   1183                     }
   1184                 }
   1185             }
   1186 
   1187             // Fallback: read the image using the normal means
   1188             BufferedImage image = ImageIO.read(file);
   1189             if (image != null) {
   1190                 return new Dimension(image.getWidth(), image.getHeight());
   1191             } else {
   1192                 return null;
   1193             }
   1194         } catch (IOException e) {
   1195             // Pass -- we can't handle all image types, warn about those we can
   1196             return null;
   1197         }
   1198     }
   1199 
   1200     // XML detector: Skim manifest
   1201 
   1202     @Override
   1203     public boolean appliesTo(Context context, File file) {
   1204         return file.getName().equals(ANDROID_MANIFEST_XML);
   1205     }
   1206 
   1207     @Override
   1208     public Collection<String> getApplicableElements() {
   1209         return Collections.singletonList(TAG_APPLICATION);
   1210     }
   1211 
   1212     @Override
   1213     public void visitElement(XmlContext context, Element element) {
   1214         assert element.getTagName().equals(TAG_APPLICATION);
   1215         mApplicationIcon = element.getAttributeNS(ANDROID_URI, ATTR_ICON);
   1216         if (mApplicationIcon.startsWith(DRAWABLE_RESOURCE_PREFIX)) {
   1217             mApplicationIcon = mApplicationIcon.substring(DRAWABLE_RESOURCE_PREFIX.length());
   1218         }
   1219     }
   1220 }
   1221