blob: 431169d2c2cdb85a62e0e9394e056a34d2263c4e [file] [log] [blame]
// 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)
.build();
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;
}
}