• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
IconDetector()257     public IconDetector() {
258     }
259 
260     @Override
getSpeed()261     public Speed getSpeed() {
262         return Speed.SLOW;
263     }
264 
265     @Override
beforeCheckProject(Context context)266     public void beforeCheckProject(Context context) {
267         mApplicationIcon = null;
268     }
269 
270     @Override
afterCheckLibraryProject(Context context)271     public void afterCheckLibraryProject(Context context) {
272         checkResourceFolder(context, context.getProject().getDir());
273     }
274 
275     @Override
afterCheckProject(Context context)276     public void afterCheckProject(Context context) {
277         checkResourceFolder(context, context.getProject().getDir());
278     }
279 
checkResourceFolder(Context context, File dir)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 
isDrawableFile(String name)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.
checkDuplicates(Context context, Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes)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.
checkDipSizes(Context context, Map<File, Dimension> pixelSizes)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 
checkDensities(Context context, File res, Map<File, Set<String>> folderToNames)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      */
nameDifferences(Set<String> a, Set<String> b)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      */
nameIntersection(Set<String> a, Set<String> b)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 
isNoDpiFolder(File file)931     private static boolean isNoDpiFolder(File file) {
932         return file.getName().contains("-nodpi");
933     }
934 
checkDrawableDir(Context context, File folder, File[] files, Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes)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 
checkExpectedSizes(Context context, File folder, File[] files)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      */
isAndroid30(Context context, int folderVersion)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      */
isAndroid23(Context context, int folderVersion)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 
getMdpiScalingFactor(String folderName)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 
checkSize(Context context, String folderName, File file, int mdpiWidth, int mdpiHeight, boolean exactMatch)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 
getSize(File file)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
appliesTo(Context context, File file)1203     public boolean appliesTo(Context context, File file) {
1204         return file.getName().equals(ANDROID_MANIFEST_XML);
1205     }
1206 
1207     @Override
getApplicableElements()1208     public Collection<String> getApplicableElements() {
1209         return Collections.singletonList(TAG_APPLICATION);
1210     }
1211 
1212     @Override
visitElement(XmlContext context, Element element)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