@Override public boolean isIgnored( @NonNull Context context, @NonNull Issue issue, @Nullable Location location, @NonNull String message, @Nullable Object data) { ensureInitialized(); String id = issue.getId(); List<String> paths = mSuppressed.get(id); if (paths == null) { paths = mSuppressed.get(VALUE_ALL); } if (paths != null && location != null) { File file = location.getFile(); String relativePath = context.getProject().getRelativePath(file); for (String suppressedPath : paths) { if (suppressedPath.equals(relativePath)) { return true; } // Also allow a prefix if (relativePath.startsWith(suppressedPath)) { return true; } } } if (mRegexps != null) { List<Pattern> regexps = mRegexps.get(id); if (regexps == null) { regexps = mRegexps.get(VALUE_ALL); } if (regexps != null && location != null) { File file = location.getFile(); String relativePath = context.getProject().getRelativePath(file); boolean checkUnixPath = false; for (Pattern regexp : regexps) { Matcher matcher = regexp.matcher(relativePath); if (matcher.find()) { return true; } else if (regexp.pattern().indexOf('/') != -1) { checkUnixPath = true; } } if (checkUnixPath && CURRENT_PLATFORM == PLATFORM_WINDOWS) { relativePath = relativePath.replace('\\', '/'); for (Pattern regexp : regexps) { Matcher matcher = regexp.matcher(relativePath); if (matcher.find()) { return true; } } } } } return mParent != null && mParent.isIgnored(context, issue, location, message, data); }
@Override public void afterCheckProject(@NonNull Context context) { if (mNames != null && mNames.size() > 0 && mHaveBytecode) { List<String> names = new ArrayList<String>(mNames.keySet()); Collections.sort(names); LintDriver driver = context.getDriver(); for (String name : names) { Handle handle = mNames.get(name); Object clientData = handle.getClientData(); if (clientData instanceof Node) { if (driver.isSuppressed(ISSUE, (Node) clientData)) { continue; } } Location location = handle.resolve(); String message = String.format( "Corresponding method handler 'public void %1$s(android.view.View)' not found", name); List<String> similar = mSimilar != null ? mSimilar.get(name) : null; if (similar != null) { Collections.sort(similar); message = message + String.format(" (did you mean %1$s ?)", Joiner.on(", ").join(similar)); } context.report(ISSUE, location, message, null); } } }
@Override public void beforeCheckProject(@NonNull Context context) { if (context.getPhase() == 1) { mFileToIds = new HashMap<File, Set<String>>(); mIncludes = new HashMap<File, List<String>>(); } }
@Override public void afterCheckFile(@NonNull Context context) { if (context.getPhase() == 1) { // Store this layout's set of ids for full project analysis in afterCheckProject mFileToIds.put(context.file, mIds); mIds = null; } }
@Override public void afterCheckProject(Context context) { // Make sure no File res = new File(context.getProject().getDir(), RES_FOLDER); if (res.isDirectory()) { File[] folders = res.listFiles(); if (folders != null) { boolean checkFolders = context.isEnabled(ICON_DENSITIES) || context.isEnabled(ICON_MISSING_FOLDER) || context.isEnabled(ICON_NODPI); boolean checkDipSizes = context.isEnabled(ICON_DIP_SIZE); boolean checkDuplicates = context.isEnabled(DUPLICATES_NAMES) || context.isEnabled(DUPLICATES_CONFIGURATIONS); Map<File, Dimension> pixelSizes = null; Map<File, Long> fileSizes = null; if (checkDipSizes || checkDuplicates) { pixelSizes = new HashMap<File, Dimension>(); fileSizes = new HashMap<File, Long>(); } Map<File, Set<String>> folderToNames = new HashMap<File, Set<String>>(); for (File folder : folders) { String folderName = folder.getName(); if (folderName.startsWith(DRAWABLE_FOLDER)) { File[] files = folder.listFiles(); if (files != null) { checkDrawableDir(context, folder, files, pixelSizes, fileSizes); if (checkFolders && DENSITY_PATTERN.matcher(folderName).matches()) { Set<String> names = new HashSet<String>(files.length); for (File f : files) { String name = f.getName(); if (hasBitmapExtension(name)) { names.add(f.getName()); } } folderToNames.put(folder, names); } } } } if (checkDipSizes) { checkDipSizes(context, pixelSizes); } if (checkDuplicates) { checkDuplicates(context, pixelSizes, fileSizes); } if (checkFolders && folderToNames.size() > 0) { checkDensities(context, res, folderToNames); } } } }
/** * Is this drawable folder for an Android 2.3 drawable? This will be the case if it specifies -v9 * or -v10, or if the minimum SDK version declared in the manifest is 9 or 10 (and it does not * specify some higher version like -v11 */ private boolean isAndroid23(Context context, int folderVersion) { if (isAndroid30(context, folderVersion)) { return false; } if (folderVersion == 9 || folderVersion == 10) { return true; } int minSdk = context.getProject().getMinSdk(); return minSdk == 9 || minSdk == 10; }
@Override public void afterCheckProject(@NonNull Context context) { if (context.getPhase() == 1) { // Look for duplicates if (!mIncludes.isEmpty()) { // Traverse all the include chains and ensure that there are no duplicates // across. if (context.isEnabled(CROSS_LAYOUT) && context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { IncludeGraph graph = new IncludeGraph(context); graph.check(); } } } else { assert context.getPhase() == 2; if (mErrors != null) { for (Occurrence occurrence : mErrors) { // assert location != null : occurrence; Location location = occurrence.location; if (location == null) { location = Location.create(occurrence.file); } else { Object clientData = location.getClientData(); if (clientData instanceof Node) { Node node = (Node) clientData; if (context.getDriver().isSuppressed(CROSS_LAYOUT, node)) { continue; } } } List<Occurrence> sorted = new ArrayList<Occurrence>(); Occurrence curr = occurrence.next; while (curr != null) { sorted.add(curr); curr = curr.next; } Collections.sort(sorted); Location prev = location; for (Occurrence o : sorted) { if (o.location != null) { prev.setSecondary(o.location); prev = o.location; } } context.report(CROSS_LAYOUT, location, occurrence.message, null); } } } }
/** * Computes the cumulative set of ids used in a given layout. We can't just depth-first-search * the graph and check the set of ids encountered along the way, because we need to detect when * multiple includes contribute the same ids. For example, if a file is included more than once, * that would result in duplicates. */ private Set<String> getIds(Layout layout, Deque<Layout> stack, Set<Layout> seen) { seen.add(layout); Set<String> layoutIds = layout.getIds(); List<Layout> includes = layout.getIncludes(); if (includes != null) { Set<String> ids = new HashSet<String>(); if (layoutIds != null) { ids.addAll(layoutIds); } stack.push(layout); Multimap<String, Set<String>> nameToIds = ArrayListMultimap.create(includes.size(), 4); for (Layout included : includes) { if (seen.contains(included)) { continue; } Set<String> includedIds = getIds(included, stack, seen); if (includedIds != null) { String layoutName = included.getLayoutName(); idCheck: for (String id : includedIds) { if (ids.contains(id)) { Collection<Set<String>> idSets = nameToIds.get(layoutName); if (idSets != null) { for (Set<String> siblingIds : idSets) { if (siblingIds.contains(id)) { // The id reference was added by a sibling, // so no need to complain (again) continue idCheck; } } } // Duplicate! Record location request for new phase. if (mLocations == null) { mErrors = new ArrayList<Occurrence>(); mLocations = ArrayListMultimap.create(); mContext .getDriver() .requestRepeat(DuplicateIdDetector.this, Scope.ALL_RESOURCES_SCOPE); } Map<Layout, Occurrence> occurrences = new HashMap<Layout, Occurrence>(); findId(layout, id, new ArrayDeque<Layout>(), occurrences, new HashSet<Layout>()); assert occurrences.size() >= 2; // Stash a request to find the given include Collection<Occurrence> values = occurrences.values(); List<Occurrence> sorted = new ArrayList<Occurrence>(values); Collections.sort(sorted); String msg = String.format( "Duplicate id %1$s, defined or included multiple " + "times in %2$s: %3$s", id, layout.getDisplayName(), sorted.toString()); // Store location request for the <include> tag Occurrence primary = new Occurrence(layout.getFile(), msg, null); Multimap<String, Occurrence> m = ArrayListMultimap.create(); m.put(layoutName, primary); mLocations.put(layout.getFile(), m); mErrors.add(primary); Occurrence prev = primary; // Now store all the included occurrences of the id for (Occurrence occurrence : values) { if (occurrence.file.equals(layout.getFile())) { occurrence.message = "Defined here"; } else { occurrence.message = String.format("Defined here, included via %1$s", occurrence.includePath); } m = ArrayListMultimap.create(); m.put(id, occurrence); mLocations.put(occurrence.file, m); // Link locations together prev.next = occurrence; prev = occurrence; } } ids.add(id); } // Store these ids such that on a conflict, we can tell when // an id was added by a single variation of this file nameToIds.put(layoutName, includedIds); } } Layout visited = stack.pop(); assert visited == layout; return ids; } else { return layoutIds; } }
@Override public void beforeCheckFile(@NonNull Context context) { if (context.getPhase() == 1) { mIds = new HashSet<String>(); } }
private void checkDrawableDir( Context context, File folder, File[] files, Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes) { if (folder.getName().equals(DRAWABLE_FOLDER) && context.isEnabled(ICON_LOCATION)) { for (File file : files) { String name = file.getName(); if (name.endsWith(DOT_XML)) { // pass - most common case, avoids checking other extensions } else if (endsWith(name, DOT_PNG) || endsWith(name, DOT_JPG) || endsWith(name, DOT_GIF)) { context.report( ICON_LOCATION, Location.create(file), String.format( "Found bitmap drawable res/drawable/%1$s in " + "densityless folder", file.getName()), null); } } } if (context.isEnabled(GIF_USAGE)) { for (File file : files) { String name = file.getName(); if (endsWith(name, DOT_GIF)) { context.report( GIF_USAGE, Location.create(file), "Using the .gif format for bitmaps is discouraged", null); } } } // Check icon sizes if (context.isEnabled(ICON_EXPECTED_SIZE)) { checkExpectedSizes(context, folder, files); } if (pixelSizes != null || fileSizes != null) { for (File file : files) { // TODO: Combine this check with the check for expected sizes such that // I don't check file sizes twice! String fileName = file.getName(); if (endsWith(fileName, DOT_PNG) || endsWith(fileName, DOT_JPG)) { // Only scan .png files (except 9-patch png's) and jpg files for // dip sizes. Duplicate checks can also be performed on ninepatch files. if (pixelSizes != null && !endsWith(fileName, DOT_9PNG)) { Dimension size = getSize(file); pixelSizes.put(file, size); } if (fileSizes != null) { fileSizes.put(file, file.length()); } } } } }
private void checkDensities(Context context, File res, Map<File, Set<String>> folderToNames) { // TODO: Is there a way to look at the manifest and figure out whether // all densities are expected to be needed? // Note: ldpi is probably not needed; it has very little usage // (about 2%; http://developer.android.com/resources/dashboard/screens.html) // TODO: Use the matrix to check out if we can eliminate densities based // on the target screens? Set<String> definedDensities = new HashSet<String>(); for (File f : folderToNames.keySet()) { definedDensities.add(f.getName()); } // Look for missing folders -- if you define say drawable-mdpi then you // should also define -hdpi and -xhdpi. if (context.isEnabled(ICON_MISSING_FOLDER)) { List<String> missing = new ArrayList<String>(); for (String density : REQUIRED_DENSITIES) { if (!definedDensities.contains(density)) { missing.add(density); } } if (missing.size() > 0) { context.report( ICON_MISSING_FOLDER, null /* location */, String.format( "Missing density variation folders in %1$s: %2$s", context.getProject().getDisplayPath(res), LintUtils.formatList(missing, missing.size())), null); } } if (context.isEnabled(ICON_NODPI)) { Set<String> noDpiNames = new HashSet<String>(); for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { if (isNoDpiFolder(entry.getKey())) { noDpiNames.addAll(entry.getValue()); } } if (noDpiNames.size() > 0) { // Make sure that none of the nodpi names appear in a non-nodpi folder Set<String> inBoth = new HashSet<String>(); List<File> files = new ArrayList<File>(); for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { File folder = entry.getKey(); String folderName = folder.getName(); if (!isNoDpiFolder(folder)) { assert DENSITY_PATTERN.matcher(folderName).matches(); Set<String> overlap = intersection(noDpiNames, entry.getValue()); inBoth.addAll(overlap); for (String name : overlap) { files.add(new File(folder, name)); } } } if (inBoth.size() > 0) { List<String> list = new ArrayList<String>(inBoth); Collections.sort(list); // Chain locations together Collections.sort(files); Location location = null; for (File file : files) { Location linkedLocation = location; location = Location.create(file); location.setSecondary(linkedLocation); } context.report( ICON_DENSITIES, location, String.format( "The following images appear in both -nodpi and in a density folder: %1$s", LintUtils.formatList(list, 10)), null); } } } if (context.isEnabled(ICON_DENSITIES)) { // Look for folders missing some of the specific assets Set<String> allNames = new HashSet<String>(); for (Entry<File, Set<String>> entry : folderToNames.entrySet()) { if (!isNoDpiFolder(entry.getKey())) { Set<String> names = entry.getValue(); allNames.addAll(names); } } for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { File file = entry.getKey(); if (isNoDpiFolder(file)) { continue; } Set<String> names = entry.getValue(); if (names.size() != allNames.size()) { List<String> delta = new ArrayList<String>(difference(allNames, names)); Collections.sort(delta); String foundIn = ""; if (delta.size() == 1) { // Produce list of where the icon is actually defined List<String> defined = new ArrayList<String>(); String name = delta.get(0); for (Map.Entry<File, Set<String>> e : folderToNames.entrySet()) { if (e.getValue().contains(name)) { defined.add(e.getKey().getName()); } } if (defined.size() > 0) { foundIn = String.format(" (found in %1$s)", LintUtils.formatList(defined, 5)); } } context.report( ICON_DENSITIES, Location.create(file), String.format( "Missing the following drawables in %1$s: %2$s%3$s", file.getName(), LintUtils.formatList(delta, 5), foundIn), null); } } } }
// This method checks the given map from resource file to pixel dimensions for each // such image and makes sure that the normalized dip sizes across all the densities // are mostly the same. private void checkDipSizes(Context context, Map<File, Dimension> pixelSizes) { // Partition up the files such that I can look at a series by name. This // creates a map from filename (such as foo.png) to a list of files // providing that icon in various folders: drawable-mdpi/foo.png, drawable-hdpi/foo.png // etc. Map<String, List<File>> nameToFiles = new HashMap<String, List<File>>(); for (File file : pixelSizes.keySet()) { String name = file.getName(); List<File> list = nameToFiles.get(name); if (list == null) { list = new ArrayList<File>(); nameToFiles.put(name, list); } list.add(file); } ArrayList<String> names = new ArrayList<String>(nameToFiles.keySet()); Collections.sort(names); // We have to partition the files further because it's possible for the project // to have different configurations for an icon, such as this: // drawable-large-hdpi/foo.png, drawable-large-mdpi/foo.png, // drawable-hdpi/foo.png, drawable-mdpi/foo.png, // drawable-hdpi-v11/foo.png and drawable-mdpi-v11/foo.png. // In this case we don't want to compare across categories; we want to // ensure that the drawable-large-{density} icons are consistent, // that the drawable-{density}-v11 icons are consistent, and that // the drawable-{density} icons are consistent. // Map from name to list of map from parent folder to list of files Map<String, Map<String, List<File>>> configMap = new HashMap<String, Map<String, List<File>>>(); for (Map.Entry<String, List<File>> entry : nameToFiles.entrySet()) { String name = entry.getKey(); List<File> files = entry.getValue(); for (File file : files) { String parentName = file.getParentFile().getName(); // Strip out the density part int index = -1; for (String qualifier : DENSITY_QUALIFIERS) { index = parentName.indexOf(qualifier); if (index != -1) { parentName = parentName.substring(0, index) + parentName.substring(index + qualifier.length()); break; } } if (index == -1) { // No relevant qualifier found in the parent directory name, // e.g. it's just "drawable" or something like "drawable-nodpi". continue; } Map<String, List<File>> folderMap = configMap.get(name); if (folderMap == null) { folderMap = new HashMap<String, List<File>>(); configMap.put(name, folderMap); } // Map from name to a map from parent folder to files List<File> list = folderMap.get(parentName); if (list == null) { list = new ArrayList<File>(); folderMap.put(parentName, list); } list.add(file); } } for (String name : names) { // List<File> files = nameToFiles.get(name); Map<String, List<File>> configurations = configMap.get(name); if (configurations == null) { // Nothing in this configuration: probably only found in drawable/ or // drawable-nodpi etc directories. continue; } for (Map.Entry<String, List<File>> entry : configurations.entrySet()) { List<File> files = entry.getValue(); // Ensure that all the dip sizes are *roughly* the same Map<File, Dimension> dipSizes = new HashMap<File, Dimension>(); int dipWidthSum = 0; // Incremental computation of average int dipHeightSum = 0; // Incremental computation of average int count = 0; for (File file : files) { float factor = getMdpiScalingFactor(file.getParentFile().getName()); if (factor > 0) { Dimension size = pixelSizes.get(file); Dimension dip = new Dimension(Math.round(size.width / factor), Math.round(size.height / factor)); dipWidthSum += dip.width; dipHeightSum += dip.height; dipSizes.put(file, dip); count++; } } if (count == 0) { // Icons in drawable/ and drawable-nodpi/ continue; } int meanWidth = dipWidthSum / count; int meanHeight = dipHeightSum / count; // Compute standard deviation? int squareWidthSum = 0; int squareHeightSum = 0; for (Dimension size : dipSizes.values()) { squareWidthSum += (size.width - meanWidth) * (size.width - meanWidth); squareHeightSum += (size.height - meanHeight) * (size.height - meanHeight); } double widthStdDev = Math.sqrt(squareWidthSum / count); double heightStdDev = Math.sqrt(squareHeightSum / count); if (widthStdDev > meanWidth / 10 || heightStdDev > meanHeight) { Location location = null; StringBuilder sb = new StringBuilder(); // Sort entries by decreasing dip size List<Map.Entry<File, Dimension>> entries = new ArrayList<Map.Entry<File, Dimension>>(); for (Map.Entry<File, Dimension> entry2 : dipSizes.entrySet()) { entries.add(entry2); } Collections.sort( entries, new Comparator<Map.Entry<File, Dimension>>() { public int compare(Entry<File, Dimension> e1, Entry<File, Dimension> e2) { Dimension d1 = e1.getValue(); Dimension d2 = e2.getValue(); if (d1.width != d2.width) { return d2.width - d1.width; } return d2.height - d1.height; } }); for (Map.Entry<File, Dimension> entry2 : entries) { if (sb.length() > 0) { sb.append(", "); } File file = entry2.getKey(); // Chain locations together Location linkedLocation = location; location = Location.create(file); location.setSecondary(linkedLocation); Dimension dip = entry2.getValue(); Dimension px = pixelSizes.get(file); String fileName = file.getParentFile().getName() + File.separator + file.getName(); sb.append( String.format( "%1$s: %2$dx%3$d dp (%4$dx%5$d px)", fileName, dip.width, dip.height, px.width, px.height)); } String message = String.format( "The image %1$s varies significantly in its density-independent (dip) " + "size across the various density versions: %2$s", name, sb.toString()); context.report(ICON_DIP_SIZE, location, message, null); } } } }
// This method looks for duplicates in the assets. This uses two pieces of information // (file sizes and image dimensions) to quickly reject candidates, such that it only // needs to check actual file contents on a small subset of the available files. private void checkDuplicates( Context context, Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes) { Map<Long, Set<File>> sameSizes = new HashMap<Long, Set<File>>(); Map<Long, File> seenSizes = new HashMap<Long, File>(fileSizes.size()); for (Map.Entry<File, Long> entry : fileSizes.entrySet()) { File file = entry.getKey(); Long size = entry.getValue(); if (seenSizes.containsKey(size)) { Set<File> set = sameSizes.get(size); if (set == null) { set = new HashSet<File>(); set.add(seenSizes.get(size)); sameSizes.put(size, set); } set.add(file); } else { seenSizes.put(size, file); } } if (sameSizes.size() == 0) { return; } // Now go through the files that have the same size and check to see if we can // split them apart based on image dimensions // Note: we may not have file sizes on all the icons; in particular, // we don't have file sizes for ninepatch files. Collection<Set<File>> candidateLists = sameSizes.values(); for (Set<File> candidates : candidateLists) { Map<Dimension, Set<File>> sameDimensions = new HashMap<Dimension, Set<File>>(candidates.size()); List<File> noSize = new ArrayList<File>(); for (File file : candidates) { Dimension dimension = pixelSizes.get(file); if (dimension != null) { Set<File> set = sameDimensions.get(dimension); if (set == null) { set = new HashSet<File>(); sameDimensions.put(dimension, set); } set.add(file); } else { noSize.add(file); } } // Files that we have no dimensions for must be compared against everything Collection<Set<File>> sets = sameDimensions.values(); if (noSize.size() > 0) { if (sets.size() > 0) { for (Set<File> set : sets) { set.addAll(noSize); } } else { // Must just test the noSize elements against themselves HashSet<File> noSizeSet = new HashSet<File>(noSize); sets = Collections.<Set<File>>singletonList(noSizeSet); } } // Map from file to actual byte contents of the file. // We store this in a map such that for repeated files, such as noSize files // which can appear in multiple buckets, we only need to read them once Map<File, byte[]> fileContents = new HashMap<File, byte[]>(); // Now we're ready for the final check where we actually check the // bits. We have to partition the files into buckets of files that // are identical. for (Set<File> set : sets) { if (set.size() < 2) { continue; } // Read all files in this set and store in map for (File file : set) { byte[] bits = fileContents.get(file); if (bits == null) { try { bits = LintUtils.readBytes(file); fileContents.put(file, bits); } catch (IOException e) { context.log(e, null); } } } // Map where the key file is known to be equal to the value file. // After we check individual files for equality this will be used // to look for transitive equality. Map<File, File> equal = new HashMap<File, File>(); // Now go and compare all the files. This isn't an efficient algorithm // but the number of candidates should be very small List<File> files = new ArrayList<File>(set); Collections.sort(files); for (int i = 0; i < files.size() - 1; i++) { for (int j = i + 1; j < files.size(); j++) { File file1 = files.get(i); File file2 = files.get(j); byte[] contents1 = fileContents.get(file1); byte[] contents2 = fileContents.get(file2); if (contents1 == null || contents2 == null) { // File couldn't be read: ignore continue; } if (contents1.length != contents2.length) { // Sizes differ: not identical. // This shouldn't happen since we've already partitioned based // on File.length(), but just make sure here since the file // system could have lied, or cached a value that has changed // if the file was just overwritten continue; } boolean same = true; for (int k = 0; k < contents1.length; k++) { if (contents1[k] != contents2[k]) { same = false; break; } } if (same) { equal.put(file1, file2); } } } if (equal.size() > 0) { Map<File, Set<File>> partitions = new HashMap<File, Set<File>>(); List<Set<File>> sameSets = new ArrayList<Set<File>>(); for (Map.Entry<File, File> entry : equal.entrySet()) { File file1 = entry.getKey(); File file2 = entry.getValue(); Set<File> set1 = partitions.get(file1); Set<File> set2 = partitions.get(file2); if (set1 != null) { set1.add(file2); } else if (set2 != null) { set2.add(file1); } else { set = new HashSet<File>(); sameSets.add(set); set.add(file1); set.add(file2); partitions.put(file1, set); partitions.put(file2, set); } } // We've computed the partitions of equal files. Now sort them // for stable output. List<List<File>> lists = new ArrayList<List<File>>(); for (Set<File> same : sameSets) { assert same.size() > 0; ArrayList<File> sorted = new ArrayList<File>(same); Collections.sort(sorted); lists.add(sorted); } // Sort overall partitions by the first item in each list Collections.sort( lists, new Comparator<List<File>>() { public int compare(List<File> list1, List<File> list2) { return list1.get(0).compareTo(list2.get(0)); } }); for (List<File> sameFiles : lists) { Location location = null; boolean sameNames = true; String lastName = null; for (File file : sameFiles) { if (lastName != null && !lastName.equals(file.getName())) { sameNames = false; } lastName = file.getName(); // Chain locations together Location linkedLocation = location; location = Location.create(file); location.setSecondary(linkedLocation); } if (sameNames) { StringBuilder sb = new StringBuilder(); for (File file : sameFiles) { if (sb.length() > 0) { sb.append(", "); // $NON-NLS-1$ } sb.append(file.getParentFile().getName()); } String message = String.format( "The %1$s icon has identical contents in the following configuration folders: %2$s", lastName, sb.toString()); context.report(DUPLICATES_CONFIGURATIONS, location, message, null); } else { StringBuilder sb = new StringBuilder(); for (File file : sameFiles) { if (sb.length() > 0) { sb.append(", "); // $NON-NLS-1$ } sb.append(file.getName()); } String message = String.format( "The following unrelated icon files have identical contents: %1$s", sb.toString()); context.report(DUPLICATES_NAMES, location, message, null); } } } } } }
private void checkSize( Context context, String folderName, File file, int mdpiWidth, int mdpiHeight, boolean exactMatch) { String fileName = file.getName(); // Only scan .png files (except 9-patch png's) and jpg files if (!((endsWith(fileName, DOT_PNG) && !endsWith(fileName, DOT_9PNG)) || endsWith(fileName, DOT_JPG))) { return; } int width = -1; int height = -1; // Use 3:4:6:8 scaling ratio to look up the other expected sizes if (folderName.startsWith(DRAWABLE_MDPI)) { width = mdpiWidth; height = mdpiHeight; } else if (folderName.startsWith(DRAWABLE_HDPI)) { // Perform math using floating point; if we just do // width = mdpiWidth * 3 / 2; // then for mdpiWidth = 25 (as in notification icons on pre-GB) we end up // with width = 37, instead of 38 (with floating point rounding we get 37.5 = 38) width = Math.round(mdpiWidth * 3.f / 2); height = Math.round(mdpiHeight * 3f / 2); } else if (folderName.startsWith(DRAWABLE_XHDPI)) { width = mdpiWidth * 2; height = mdpiHeight * 2; } else if (folderName.startsWith(DRAWABLE_LDPI)) { width = Math.round(mdpiWidth * 3f / 4); height = Math.round(mdpiHeight * 3f / 4); } else { return; } Dimension size = getSize(file); if (size != null) { if (exactMatch && size.width != width || size.height != height) { context.report( ICON_EXPECTED_SIZE, Location.create(file), String.format( "Incorrect icon size for %1$s: expected %2$dx%3$d, but was %4$dx%5$d", folderName + File.separator + file.getName(), width, height, size.width, size.height), null); } else if (!exactMatch && size.width > width || size.height > height) { context.report( ICON_EXPECTED_SIZE, Location.create(file), String.format( "Incorrect icon size for %1$s: icon size should be at most %2$dx%3$d, but was %4$dx%5$d", folderName + File.separator + file.getName(), width, height, size.width, size.height), null); } } }
/** * Is this drawable folder for an Android 3.0 drawable? This will be the case if it specifies * -v11+, or if the minimum SDK version declared in the manifest is at least 11. */ private boolean isAndroid30(Context context, int folderVersion) { return folderVersion >= 11 || context.getProject().getMinSdk() >= 11; }