Exemple #1
0
  @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;
 }