| // Copyright 2015 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.android; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.Collections2; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.Ordering; |
| import com.google.devtools.build.android.AndroidResourceMerger.MergingException; |
| import java.io.IOException; |
| import java.nio.file.FileVisitOption; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.LinkOption; |
| import java.nio.file.Path; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.List; |
| import javax.annotation.Nullable; |
| |
| /** Filters a {@link MergedAndroidData} resource drawables to the specified densities. */ |
| public class DensitySpecificResourceFilter { |
| private static class ResourceInfo { |
| /** Path to an actual file resource, instead of a directory. */ |
| private Path resource; |
| |
| private String restype; |
| private String qualifiers; |
| private String density; |
| private String resid; |
| |
| public ResourceInfo( |
| Path resource, String restype, String qualifiers, String density, String resid) { |
| this.resource = resource; |
| this.restype = restype; |
| this.qualifiers = qualifiers; |
| this.density = density; |
| this.resid = resid; |
| } |
| |
| public Path getResource() { |
| return this.resource; |
| } |
| |
| public String getRestype() { |
| return this.restype; |
| } |
| |
| public String getQualifiers() { |
| return this.qualifiers; |
| } |
| |
| public String getDensity() { |
| return this.density; |
| } |
| |
| public String getResid() { |
| return this.resid; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("resource", resource) |
| .add("restype", restype) |
| .add("qualifiers", qualifiers) |
| .add("density", density) |
| .add("resid", resid) |
| .toString(); |
| } |
| } |
| |
| private static class RecursiveFileCopier extends SimpleFileVisitor<Path> { |
| private final Path copyToPath; |
| private final List<Path> copiedSourceFiles = new ArrayList<>(); |
| private Path root; |
| |
| public RecursiveFileCopier(final Path copyToPath, final Path root) { |
| this.copyToPath = copyToPath; |
| this.root = root; |
| } |
| |
| @Override |
| public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { |
| Path copyTo = copyToPath.resolve(root.relativize(path)); |
| Files.createDirectories(copyTo.getParent()); |
| Files.copy(path, copyTo, LinkOption.NOFOLLOW_LINKS); |
| copiedSourceFiles.add(copyTo); |
| return FileVisitResult.CONTINUE; |
| } |
| |
| public List<Path> getCopiedFiles() { |
| return copiedSourceFiles; |
| } |
| } |
| |
| private final List<String> densities; |
| private final Path out; |
| private final Path working; |
| |
| private static final ImmutableMap<String, Integer> DENSITY_MAP = |
| new ImmutableMap.Builder<String, Integer>() |
| .put("nodpi", 0) |
| .put("ldpi", 120) |
| .put("mdpi", 160) |
| .put("tvdpi", 213) |
| .put("hdpi", 240) |
| .put("280dpi", 280) |
| .put("xhdpi", 320) |
| .put("340dpi", 340) |
| .put("400dpi", 400) |
| .put("420dpi", 420) |
| .put("xxhdpi", 480) |
| .put("560dpi", 560) |
| .put("xxxhdpi", 640) |
| .buildOrThrow(); |
| |
| private static final Function<ResourceInfo, String> GET_RESOURCE_ID = |
| new Function<ResourceInfo, String>() { |
| @Override |
| public String apply(ResourceInfo info) { |
| return info.getResid(); |
| } |
| }; |
| |
| private static final Function<ResourceInfo, String> GET_RESOURCE_QUALIFIERS = |
| new Function<ResourceInfo, String>() { |
| @Override |
| public String apply(ResourceInfo info) { |
| return info.getQualifiers(); |
| } |
| }; |
| |
| private static final Function<ResourceInfo, Path> GET_RESOURCE_PATH = |
| new Function<ResourceInfo, Path>() { |
| @Override |
| public Path apply(ResourceInfo info) { |
| return info.getResource(); |
| } |
| }; |
| |
| /** |
| * @param densities An array of string densities to use for filtering resources |
| * @param out The path to use for name spacing the final resource directory. |
| * @param working The path of the working directory for the filtering |
| */ |
| public DensitySpecificResourceFilter(List<String> densities, Path out, Path working) |
| throws MergingException { |
| this.densities = densities; |
| this.out = out; |
| this.working = working; |
| |
| for (String density : densities) { |
| if (!DENSITY_MAP.containsKey(density)) { |
| throw MergingException.withMessage(density + " is not a known density qualifier."); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| List<Path> getResourceToRemove(List<Path> resourcePaths) { |
| Predicate<ResourceInfo> requestedDensityFilter = |
| new Predicate<ResourceInfo>() { |
| @Override |
| public boolean apply(@Nullable ResourceInfo info) { |
| return !densities.contains(info.getDensity()); |
| } |
| }; |
| |
| List<ResourceInfo> resourceInfos = getResourceInfos(resourcePaths); |
| List<ResourceInfo> densityResourceInfos = filterDensityResourceInfos(resourceInfos); |
| List<ResourceInfo> resourceInfoToRemove = new ArrayList<>(); |
| |
| Multimap<String, ResourceInfo> fileGroups = |
| groupResourceInfos(densityResourceInfos, GET_RESOURCE_ID); |
| |
| for (String key : fileGroups.keySet()) { |
| Multimap<String, ResourceInfo> qualifierGroups = |
| groupResourceInfos(fileGroups.get(key), GET_RESOURCE_QUALIFIERS); |
| |
| for (String qualifiers : qualifierGroups.keySet()) { |
| Collection<ResourceInfo> qualifierResourceInfos = qualifierGroups.get(qualifiers); |
| |
| if (qualifierResourceInfos.size() != 1) { |
| List<ResourceInfo> sortedResourceInfos = |
| Ordering.natural() |
| .onResultOf( |
| new Function<ResourceInfo, Double>() { |
| @Override |
| public Double apply(ResourceInfo info) { |
| return matchScore(info, densities); |
| } |
| }) |
| .immutableSortedCopy(qualifierResourceInfos); |
| resourceInfoToRemove.addAll( |
| Collections2.filter( |
| sortedResourceInfos.subList(1, sortedResourceInfos.size()), |
| requestedDensityFilter)); |
| } |
| } |
| } |
| |
| return ImmutableList.copyOf(Lists.transform(resourceInfoToRemove, GET_RESOURCE_PATH)); |
| } |
| |
| private static void removeResources(List<Path> resourceInfoToRemove) { |
| for (Path resource : resourceInfoToRemove) { |
| resource.toFile().delete(); |
| } |
| } |
| |
| private static Multimap<String, ResourceInfo> groupResourceInfos( |
| final Collection<ResourceInfo> resourceInfos, Function<ResourceInfo, String> keyFunction) { |
| Multimap<String, ResourceInfo> resourceGroups = ArrayListMultimap.create(); |
| |
| for (ResourceInfo resourceInfo : resourceInfos) { |
| resourceGroups.put(keyFunction.apply(resourceInfo), resourceInfo); |
| } |
| |
| return ImmutableMultimap.copyOf(resourceGroups); |
| } |
| |
| private static List<ResourceInfo> getResourceInfos(final List<Path> resourcePaths) { |
| List<ResourceInfo> resourceInfos = new ArrayList<>(); |
| |
| for (Path resourcePath : resourcePaths) { |
| String qualifiers = resourcePath.getParent().getFileName().toString(); |
| String density = ""; |
| |
| for (String densityName : DENSITY_MAP.keySet()) { |
| if (qualifiers.contains("-" + densityName)) { |
| qualifiers = qualifiers.replace("-" + densityName, ""); |
| density = densityName; |
| } |
| } |
| |
| String[] qualifierArray = qualifiers.split("-"); |
| String restype = qualifierArray[0]; |
| qualifiers = |
| qualifierArray.length > 0 |
| ? Joiner.on("-").join(Arrays.copyOfRange(qualifierArray, 1, qualifierArray.length)) |
| : ""; |
| resourceInfos.add( |
| new ResourceInfo( |
| resourcePath, restype, qualifiers, density, resourcePath.getFileName().toString())); |
| } |
| |
| return ImmutableList.copyOf(resourceInfos); |
| } |
| |
| private static List<ResourceInfo> filterDensityResourceInfos( |
| final List<ResourceInfo> resourceInfos) { |
| List<ResourceInfo> densityResourceInfos = new ArrayList<>(); |
| |
| for (ResourceInfo info : resourceInfos) { |
| if (info.getRestype().equals("drawable") |
| && !info.getDensity().equals("") |
| && !info.getDensity().equals("nodpi") |
| && !info.getResid().endsWith(".xml")) { |
| densityResourceInfos.add(info); |
| } |
| } |
| |
| return ImmutableList.copyOf(densityResourceInfos); |
| } |
| |
| private static double matchScore(ResourceInfo resource, List<String> densities) { |
| double score = 0; |
| for (String density : densities) { |
| score += computeAffinity(DENSITY_MAP.get(resource.getDensity()), DENSITY_MAP.get(density)); |
| } |
| return score; |
| } |
| |
| private static double computeAffinity(int resourceDensity, int density) { |
| if (resourceDensity == density) { |
| // Exact match is the best. |
| return -2; |
| } else if (resourceDensity == 2 * density) { |
| // It's very efficient to downsample an image that's exactly 2x the screen |
| // density, so we prefer that over other non-perfect matches. |
| return -1; |
| } else { |
| double affinity = Math.log((double) density / resourceDensity) / Math.log(2); |
| |
| // We give a slight bump to images that have the same multiplier but are higher quality. |
| if (affinity < 0) { |
| affinity = Math.abs(affinity) - 0.01; |
| } |
| return affinity; |
| } |
| } |
| |
| /** Filters the contents of a resource directory. */ |
| public Path filter(Path unFilteredResourceDir) { |
| // no densities to filter, so skip. |
| if (densities.isEmpty()) { |
| return unFilteredResourceDir; |
| } |
| final Path filteredResourceDir = out.resolve(working.relativize(unFilteredResourceDir)); |
| RecursiveFileCopier fileVisitor = |
| new RecursiveFileCopier(filteredResourceDir, unFilteredResourceDir); |
| try { |
| Files.walkFileTree( |
| unFilteredResourceDir, |
| EnumSet.of(FileVisitOption.FOLLOW_LINKS), |
| Integer.MAX_VALUE, |
| fileVisitor); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| removeResources(getResourceToRemove(fileVisitor.getCopiedFiles())); |
| return filteredResourceDir; |
| } |
| } |