diff options
Diffstat (limited to 'src')
11 files changed, 737 insertions, 277 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java index ad872d4e0e..32e2aa5b70 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java @@ -237,10 +237,9 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { resourceDeps, ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_R_TXT), null, /* Artifact symbolsTxt */ - ResourceConfigurationFilter.fromRuleContext(ruleContext), + ResourceFilter.fromRuleContext(ruleContext), ruleContext.getTokenizedStringListAttr("nocompress_extensions"), ruleContext.attributes().get("crunch_png", Type.BOOLEAN), - ruleContext.getTokenizedStringListAttr("densities"), false, /* incremental */ ProguardHelper.getProguardConfigArtifact(ruleContext, ""), createMainDexProguardSpec(ruleContext), @@ -261,10 +260,9 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { resourceDeps, null, /* Artifact rTxt */ null, /* Artifact symbolsTxt */ - ResourceConfigurationFilter.fromRuleContext(ruleContext), + ResourceFilter.fromRuleContext(ruleContext), ruleContext.getTokenizedStringListAttr("nocompress_extensions"), ruleContext.attributes().get("crunch_png", Type.BOOLEAN), - ruleContext.getTokenizedStringListAttr("densities"), true, /* incremental */ ProguardHelper.getProguardConfigArtifact(ruleContext, "incremental"), null, /* mainDexProguardCfg */ @@ -284,10 +282,9 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { resourceDeps, null, /* Artifact rTxt */ null, /* Artifact symbolsTxt */ - ResourceConfigurationFilter.fromRuleContext(ruleContext), + ResourceFilter.fromRuleContext(ruleContext), ruleContext.getTokenizedStringListAttr("nocompress_extensions"), ruleContext.attributes().get("crunch_png", Type.BOOLEAN), - ruleContext.getTokenizedStringListAttr("densities"), true, /* incremental */ ProguardHelper.getProguardConfigArtifact(ruleContext, "instant_run"), null, /* mainDexProguardCfg */ @@ -307,10 +304,9 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { resourceDeps, null, /* Artifact rTxt */ null, /* Artifact symbolsTxt */ - ResourceConfigurationFilter.fromRuleContext(ruleContext), + ResourceFilter.fromRuleContext(ruleContext), ruleContext.getTokenizedStringListAttr("nocompress_extensions"), ruleContext.attributes().get("crunch_png", Type.BOOLEAN), - ruleContext.getTokenizedStringListAttr("densities"), true, /* incremental */ ProguardHelper.getProguardConfigArtifact(ruleContext, "incremental_split"), null, /* mainDexProguardCfg */ @@ -1164,7 +1160,8 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { .withProguardMapping(proguardOutput.getMapping()) .withPrimary(resourceApk.getPrimaryResource()) .withDependencies(resourceApk.getResourceDependencies()) - .setConfigurationFilters(ResourceConfigurationFilter.fromRuleContext(ruleContext)) + .setResourceFilter(ResourceFilter.fromRuleContext(ruleContext)) + .setUncompressedExtensions( ruleContext.getTokenizedStringListAttr("nocompress_extensions")) .build(); @@ -1773,16 +1770,14 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { * <p>The resources should be regenerated (using aapt) if any of the following are true: * <ul> * <li>There is more than one resource container - * <li>There are densities to filter by. - * <li>There are resource configuration filters. + * <li>There are resource filters. * <li>There are extensions that should be compressed. * </ul> */ public static boolean shouldRegenerate(RuleContext ruleContext, ResourceDependencies resourceDeps) { return Iterables.size(resourceDeps.getResources()) > 1 - || ruleContext.attributes().isAttributeValueExplicitlySpecified("densities") - || ResourceConfigurationFilter.hasFilters(ruleContext) + || ResourceFilter.hasFilters(ruleContext) || ruleContext.attributes().isAttributeValueExplicitlySpecified("nocompress_extensions"); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryOnlyRule.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryOnlyRule.java index d4bb19487f..3d8efbaade 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryOnlyRule.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryOnlyRule.java @@ -84,7 +84,7 @@ public final class AndroidBinaryOnlyRule implements RuleDefinition { A list of resource configuration filters, such 'en' that will limit the resources in the apk to only the ones in the 'en' configuration. <!-- #END_BLAZE_RULE.ATTRIBUTE --> */ - .add(attr(ResourceConfigurationFilter.ATTR_NAME, STRING_LIST)) + .add(attr(ResourceFilter.RESOURCE_CONFIGURATION_FILTERS_NAME, STRING_LIST)) /* <!-- #BLAZE_RULE(android_binary).ATTRIBUTE(shrink_resources) --> Whether to perform resource shrinking. Resources that are not used by the binary will be removed from the APK. This is only supported for rules using local resources (i.e. the @@ -116,7 +116,7 @@ public final class AndroidBinaryOnlyRule implements RuleDefinition { section will also be added to the manifest if it does not already contain a superset listing. <!-- #END_BLAZE_RULE.ATTRIBUTE --> */ - .add(attr("densities", STRING_LIST)) + .add(attr(ResourceFilter.DENSITIES_NAME, STRING_LIST)) .add(attr("$android_manifest_merge_tool", LABEL) .cfg(HOST) .exec() diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidLibrary.java index f573132b03..ef4ff5295b 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidLibrary.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidLibrary.java @@ -97,10 +97,9 @@ public abstract class AndroidLibrary implements RuleConfiguredTargetFactory { ResourceDependencies.fromRuleDeps(ruleContext, JavaCommon.isNeverLink(ruleContext)), ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_R_TXT), ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_MERGED_SYMBOLS), - ResourceConfigurationFilter.empty(ruleContext), + ResourceFilter.empty(ruleContext), ImmutableList.<String>of(), /* uncompressedExtensions */ false, /* crunchPng */ - ImmutableList.<String>of(), /* densities */ false, /* incremental */ null, /* proguardCfgOut */ null, /* mainDexProguardCfg */ diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java index 75fbfa697a..6bee442d83 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java @@ -69,13 +69,12 @@ public class AndroidResourcesProcessorBuilder { private Artifact rTxtOut; private Artifact sourceJarOut; private boolean debug = false; - private ResourceConfigurationFilter resourceConfigs; + private ResourceFilter resourceFilter; private List<String> uncompressedExtensions = Collections.emptyList(); private Artifact apkOut; private final AndroidSdkProvider sdk; private List<String> assetsToIgnore = Collections.emptyList(); private SpawnAction.Builder spawnActionBuilder; - private List<String> densities = Collections.emptyList(); private String customJavaPackage; private final RuleContext ruleContext; private String versionCode; @@ -98,7 +97,7 @@ public class AndroidResourcesProcessorBuilder { this.sdk = AndroidSdkProvider.fromRuleContext(ruleContext); this.ruleContext = ruleContext; this.spawnActionBuilder = new SpawnAction.Builder(); - this.resourceConfigs = ResourceConfigurationFilter.empty(ruleContext); + this.resourceFilter = ResourceFilter.empty(ruleContext); } /** @@ -136,14 +135,9 @@ public class AndroidResourcesProcessorBuilder { return this; } - public AndroidResourcesProcessorBuilder setDensities(List<String> densities) { - this.densities = densities; - return this; - } - - public AndroidResourcesProcessorBuilder setConfigurationFilters( - ResourceConfigurationFilter resourceConfigs) { - this.resourceConfigs = resourceConfigs; + public AndroidResourcesProcessorBuilder setResourceFilter( + ResourceFilter resourceFilter) { + this.resourceFilter = resourceFilter; return this; } @@ -215,7 +209,7 @@ public class AndroidResourcesProcessorBuilder { public ResourceContainer build(ActionConstructionContext context) { List<Artifact> outs = new ArrayList<>(); CustomCommandLine.Builder builder = new CustomCommandLine.Builder(); - + // Set the busybox tool. builder.add("--tool").add("PACKAGE").add("--"); @@ -285,11 +279,15 @@ public class AndroidResourcesProcessorBuilder { builder.addExecPath("--packagePath", apkOut); outs.add(apkOut); } - if (!resourceConfigs.isEmpty()) { - builder.add("--resourceConfigs").add(resourceConfigs.getFilterString()); + if (resourceFilter.hasConfigurationFilters() && !resourceFilter.isPrefiltering()) { + builder.add("--resourceConfigs").add(resourceFilter.getConfigurationFilterString()); + } + if (resourceFilter.hasDensities() && !resourceFilter.isPrefiltering()) { + builder.add("--densities").add(resourceFilter.getDensityString()); } - if (!densities.isEmpty()) { - builder.addJoinStrings("--densities", ",", densities); + ImmutableList<String> filteredResources = resourceFilter.getFilteredResources(); + if (!filteredResources.isEmpty()) { + builder.addJoinStrings("--prefilteredResources", ",", filteredResources); } if (!uncompressedExtensions.isEmpty()) { builder.addJoinStrings("--uncompressedExtensions", ",", uncompressedExtensions); diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java b/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java index 32e78d86e4..441b0c4b12 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java @@ -300,10 +300,9 @@ public final class ApplicationManifest { ruleContext, false, /* isLibrary */ resourceDeps, - ResourceConfigurationFilter.empty(ruleContext), + ResourceFilter.empty(ruleContext), ImmutableList.<String>of(), /* uncompressedExtensions */ true, /* crunchPng */ - ImmutableList.<String>of(), /* densities */ incremental, ResourceContainer.builderFromRule(ruleContext) .setAssetsAndResourcesFrom(data) @@ -348,10 +347,9 @@ public final class ApplicationManifest { ruleContext, true, /* isLibrary */ resourceDeps, - ResourceConfigurationFilter.empty(ruleContext), + ResourceFilter.empty(ruleContext), ImmutableList.<String>of(), /* List<String> uncompressedExtensions */ false, /* crunchPng */ - ImmutableList.<String>of(), /* List<String> densities */ false, /* incremental */ resourceContainer.build(), data, @@ -373,10 +371,9 @@ public final class ApplicationManifest { ResourceDependencies resourceDeps, Artifact rTxt, Artifact symbols, - ResourceConfigurationFilter configurationFilters, + ResourceFilter resourceFilter, List<String> uncompressedExtensions, boolean crunchPng, - List<String> densities, boolean incremental, Artifact proguardCfg, @Nullable Artifact mainDexProguardCfg, @@ -405,10 +402,9 @@ public final class ApplicationManifest { ruleContext, isLibrary, resourceDeps, - configurationFilters, + resourceFilter, uncompressedExtensions, crunchPng, - densities, incremental, ResourceContainer.builderFromRule(ruleContext) .setAssetsAndResourcesFrom(data) @@ -431,10 +427,9 @@ public final class ApplicationManifest { RuleContext ruleContext, boolean isLibrary, ResourceDependencies resourceDeps, - ResourceConfigurationFilter configurationFilters, + ResourceFilter resourceFilter, List<String> uncompressedExtensions, boolean crunchPng, - List<String> densities, boolean incremental, ResourceContainer maybeInlinedResourceContainer, LocalResourceContainer data, @@ -455,11 +450,11 @@ public final class ApplicationManifest { if (ruleContext.hasErrors()) { return null; } - + // Filter the resources during analysis to prevent processing of and dependencies on unwanted // resources during execution. - resourceContainer = resourceContainer.filter(configurationFilters); - resourceDeps = resourceDeps.filter(configurationFilters); + resourceContainer = resourceContainer.filter(resourceFilter); + resourceDeps = resourceDeps.filter(resourceFilter); ResourceContainer processed; if (isLibrary && AndroidCommon.getAndroidConfig(ruleContext).useParallelResourceProcessing()) { @@ -507,7 +502,7 @@ public final class ApplicationManifest { new AndroidResourcesProcessorBuilder(ruleContext) .setLibrary(isLibrary) .setApkOut(resourceContainer.getApk()) - .setConfigurationFilters(configurationFilters) + .setResourceFilter(resourceFilter) .setUncompressedExtensions(uncompressedExtensions) .setCrunchPng(crunchPng) .setJavaPackage(resourceContainer.getJavaPackage()) @@ -516,7 +511,6 @@ public final class ApplicationManifest { .setMergedResourcesOut(mergedResources) .withPrimary(resourceContainer) .withDependencies(resourceDeps) - .setDensities(densities) .setProguardOut(proguardCfg) .setMainDexProguardOut(mainDexProguardCfg) .setDataBindingInfoZip(dataBindingInfoZip) @@ -623,7 +617,8 @@ public final class ApplicationManifest { AndroidAaptActionHelper aaptActionHelper = new AndroidAaptActionHelper(ruleContext, getManifest(), Lists.newArrayList(resourceContainers)); - String resourceConfigurationFilters = ResourceConfigurationFilter.extractFilters(ruleContext); + ResourceFilter resourceFilter = ResourceFilter.fromRuleContext(ruleContext); + List<String> uncompressedExtensions = ruleContext.getTokenizedStringListAttr("nocompress_extensions"); @@ -632,8 +627,8 @@ public final class ApplicationManifest { for (String extension : uncompressedExtensions) { additionalAaptOpts.add("-0").add(extension); } - if (!resourceConfigurationFilters.isEmpty()) { - additionalAaptOpts.add("-c").add(resourceConfigurationFilters); + if (resourceFilter.hasConfigurationFilters() && !resourceFilter.isPrefiltering()) { + additionalAaptOpts.add("-c").add(resourceFilter.getConfigurationFilterString()); } Artifact javaSourcesJar = null; @@ -645,9 +640,11 @@ public final class ApplicationManifest { javaSourcesJar, null, resourceContainer.getJavaPackage(), true); } - List<String> densities = ruleContext.getTokenizedStringListAttr("densities"); - aaptActionHelper.createGenerateApkAction(resourceApk, - resourceContainer.getRenameManifestPackage(), additionalAaptOpts.build(), densities); + aaptActionHelper.createGenerateApkAction( + resourceApk, + resourceContainer.getRenameManifestPackage(), + additionalAaptOpts.build(), + resourceFilter.getDensities()); ResourceContainer updatedResources = resourceContainer diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceConfigurationFilter.java b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceConfigurationFilter.java deleted file mode 100644 index fb27cdd833..0000000000 --- a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceConfigurationFilter.java +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2017 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.lib.rules.android; - -import com.android.ide.common.resources.configuration.FolderConfiguration; -import com.android.ide.common.resources.configuration.VersionQualifier; -import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.devtools.build.lib.actions.Artifact; -import com.google.devtools.build.lib.analysis.RuleContext; -import com.google.devtools.build.lib.collect.nestedset.NestedSet; -import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; - -/** Filters resources based on their qualifiers. */ -public class ResourceConfigurationFilter { - public static final String ATTR_NAME = "resource_configuration_filters"; - - /** The current {@link RuleContext}, used for reporting errors. */ - private final RuleContext ruleContext; - - /** - * A list of filters that should be applied during the analysis phase. If resources should not be - * filtered in analysis (e.g., if prefilter_resources = 0), this list should be empty. - */ - private final ImmutableList<FolderConfiguration> filters; - - /** - * The raw value of the resource_configuration_filters attribute, as a string of comma-separated - * qualifier strings. This value is passed directly as input to resource processing utilities that - * run in the execution phase, so it may represent different qualifiers than those in {@link - * #filters} if resources should not be pre-filtered during analysis. - */ - private final String filterString; - - /** - * @param ruleContext the current {@link RuleContext}, used for reporting errors - * @param filters a list of filters that should be applied during analysis time. If resources - * should not be filtered in analysis (e.g., if prefilter_resources = 0), this list should be - * empty. - * @param filterString the raw value of the resource configuration filters, as a comma-seperated - * string - */ - private ResourceConfigurationFilter( - RuleContext ruleContext, ImmutableList<FolderConfiguration> filters, String filterString) { - this.ruleContext = ruleContext; - this.filters = filters; - this.filterString = filterString; - } - - static boolean hasFilters(RuleContext ruleContext) { - return ruleContext.attributes().isAttributeValueExplicitlySpecified(ATTR_NAME); - } - - /** - * Extracts the filters from the current RuleContext, as a string. - * - * <p>In BUILD files, filters can be represented as a list of strings, a single comma-seperated - * string, or a combination of both. This method outputs a single comma-seperated string of - * filters, which can then be passed directly to resource processing actions. - * - * @param ruleContext the current {@link RuleContext} - * @return the resource configuration filters contained in the {@link RuleContext}, in a single - * comma-separated string, or an empty string if no filters exist. - */ - static String extractFilters(RuleContext ruleContext) { - if (!hasFilters(ruleContext)) { - return ""; - } - - return Joiner.on(',').join(ruleContext.getTokenizedStringListAttr(ATTR_NAME)); - } - - static ResourceConfigurationFilter fromRuleContext(RuleContext ruleContext) { - Preconditions.checkNotNull(ruleContext); - - String resourceConfigurationFilters = extractFilters(ruleContext); - - if (resourceConfigurationFilters.isEmpty()) { - return empty(ruleContext); - } - - boolean shouldPrefilter = - ruleContext.getFragment(AndroidConfiguration.class).useResourcePrefiltering(); - - if (!shouldPrefilter) { - return new ResourceConfigurationFilter( - ruleContext, ImmutableList.<FolderConfiguration>of(), resourceConfigurationFilters); - } - - ImmutableList.Builder<FolderConfiguration> builder = ImmutableList.builder(); - - for (String filter : resourceConfigurationFilters.split(",")) { - FolderConfiguration folderConfig = FolderConfiguration.getConfigForQualifierString(filter); - - if (folderConfig == null) { - ruleContext.attributeError( - ATTR_NAME, "String '" + filter + "' is not a valid resource configuration filter"); - } else { - builder.add(folderConfig); - } - } - - return new ResourceConfigurationFilter( - ruleContext, builder.build(), resourceConfigurationFilters); - } - - static ResourceConfigurationFilter empty(RuleContext ruleContext) { - return new ResourceConfigurationFilter( - ruleContext, ImmutableList.<FolderConfiguration>of(), ""); - } - - NestedSet<ResourceContainer> filter(NestedSet<ResourceContainer> resources) { - if (filters.isEmpty()) { - /* - * If the filter is empty or resource prefiltering is disabled, just return the original, - * rather than make a copy. - * - * Resources should only be prefiltered in top-level android targets (such as android_binary). - * The output of resource processing, which includes the input NestedSet<ResourceContainer> - * returned by this method, is exposed to other actions via the AndroidResourcesProvider. If - * this method did a no-op copy and collapse in those cases, rather than just return the - * original NestedSet, we would lose all of the advantages around memory and time that - * NestedSets provide: each android_library target would have to copy the resources provided - * by its dependencies into a new NestedSet rather than just create a NestedSet pointing at - * its dependencies's NestedSets. - */ - return resources; - } - - NestedSetBuilder<ResourceContainer> builder = new NestedSetBuilder<>(resources.getOrder()); - - for (ResourceContainer resource : resources) { - builder.add(resource.filter(this)); - } - - return builder.build(); - } - - ImmutableList<Artifact> filter(ImmutableList<Artifact> artifacts) { - if (filters.isEmpty()) { - return artifacts; - } - - ImmutableList.Builder<Artifact> builder = ImmutableList.builder(); - - for (Artifact artifact : artifacts) { - String containingFolder = artifact.getPath().getParentDirectory().getBaseName(); - if (matches(containingFolder)) { - builder.add(artifact); - } - } - - return builder.build(); - } - - private boolean matches(String containingFolder) { - FolderConfiguration config = FolderConfiguration.getConfigForFolder(containingFolder); - - if (config == null) { - ruleContext.ruleError( - "Resource folder '" + containingFolder + "' has invalid resource qualifiers"); - - return true; - } - - // aapt explicitly ignores the version qualifier; duplicate this behavior here. - config.setVersionQualifier(VersionQualifier.getQualifier("")); - - for (FolderConfiguration filter : filters) { - if (config.isMatchFor(filter)) { - return true; - } - } - - return false; - } - - /** - * Returns if this object represents an empty filter. - * - * <p>Note that non-empty filters are not guaranteed to filter resources during the analysis - * phase. - */ - boolean isEmpty() { - return filterString.isEmpty(); - } - - String getFilterString() { - return filterString; - } -} diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainer.java b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainer.java index c89de68856..a18daa0027 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainer.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainer.java @@ -128,7 +128,7 @@ public abstract class ResourceContainer { /** * Returns a copy of this container with filtered resources. The original container is unchanged. */ - public ResourceContainer filter(ResourceConfigurationFilter filter) { + public ResourceContainer filter(ResourceFilter filter) { return toBuilder().setResources(filter.filter(getResources())).build(); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceDependencies.java b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceDependencies.java index 03f85b3556..72e76c8763 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceDependencies.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceDependencies.java @@ -29,7 +29,7 @@ import com.google.devtools.build.lib.packages.BuildType; * abstraction simplifies the process of managing and exporting the direct and transitive resource * dependencies of an android rule, as well as providing type safety. * - * <p>The transitive and direct dependencies are not guaranteed to be disjoint. If a + * <p>The transitive and direct dependencies are not guaranteed to be disjoint. If a * library is included in both the transitive and direct dependencies, it will appear twice. This * requires consumers to manage duplicated resources gracefully. */ @@ -42,7 +42,7 @@ public final class ResourceDependencies { private final NestedSet<ResourceContainer> transitiveResources; /** * Contains all the direct dependencies of the current target. Since a given direct dependency can - * act as a "forwarding" library, collecting all the direct resource from it's dependencies + * act as a "forwarding" library, collecting all the direct resource from it's dependencies * and providing them as "direct" dependencies to maintain merge order, this uses a NestedSet to * properly maintain ordering and ease of merging. */ @@ -151,9 +151,9 @@ public final class ResourceDependencies { this.transitiveResources = transitiveResources; this.directResources = directResources; } - + /** Returns a copy of this instance with filtered resources. The original object is unchanged. */ - public ResourceDependencies filter(ResourceConfigurationFilter filter) { + public ResourceDependencies filter(ResourceFilter filter) { return new ResourceDependencies( neverlink, filter.filter(transitiveResources), filter.filter(directResources)); } diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceFilter.java b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceFilter.java new file mode 100644 index 0000000000..e7652efbba --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceFilter.java @@ -0,0 +1,502 @@ +// Copyright 2017 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.lib.rules.android; + +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.VersionQualifier; +import com.android.resources.Density; +import com.android.resources.ResourceFolderType; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.collect.nestedset.NestedSet; +import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; +import com.google.devtools.build.lib.syntax.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Filters resources based on their qualifiers. + * + * <p>This includes filtering resources based on both the "resource_configuration_filters" and + * "densities" attributes. + */ +public class ResourceFilter { + public static final String RESOURCE_CONFIGURATION_FILTERS_NAME = "resource_configuration_filters"; + public static final String DENSITIES_NAME = "densities"; + + /** The current {@link RuleContext}, used for reporting errors. */ + private final RuleContext ruleContext; + + /** + * A list of configuration filters that should be applied during the analysis phase. If resources + * should not be filtered in analysis (e.g., if android_binary.prefilter_resources is set to 0), + * this list should be empty. + */ + private final ImmutableList<FolderConfiguration> configurationFilters; + + /** + * The value of the {@link #RESOURCE_CONFIGURATION_FILTERS_NAME} attribute, as a list of qualifier + * strings. This value is passed directly as input to resource processing utilities that run in + * the execution phase, so it may represent different qualifiers than those in {@link + * #configurationFilters} if resources should not be pre-filtered during analysis. + */ + private final ImmutableList<String> configurationFilterStrings; + + /** + * A list of density filters that should be applied during the analysis phase. If resources should + * not be filtered in analysis (e.g., if prefilter_resources = 0), this list should be empty. + */ + private final ImmutableList<Density> densities; + + /** + * The value of the {@link #DENSITIES_NAME} attribute, as a list of qualifier strings. This value + * is passed directly as input to resource processing utilities that run in the execution phase, + * so it may represent different qualifiers than those in {@link #densities} if resources should + * not be pre-filtered during analysis. + */ + private final ImmutableList<String> densityStrings; + + private final ImmutableSet.Builder<String> filteredResources = ImmutableSet.builder(); + + /** + * Constructor. + * + * @param ruleContext the current {@link RuleContext}, used for reporting errors + * @param configurationFilters a list of configuration filters that should be applied during + * analysis time. If resources should not be filtered in analysis (e.g., if + * prefilter_resources = 0), this list should be empty. + * @param configurationFilterStrings the resource configuration filters, as a list of strings. + * @param densities a list of densities that should be applied to filter resources during analysis + * time. If resources should not be filtered in analysis (e.g., if prefilter_resources = 0), + * this list should be empty. + * @param densityStrings the densities, as a list of strings. + */ + private ResourceFilter( + RuleContext ruleContext, + ImmutableList<FolderConfiguration> configurationFilters, + ImmutableList<String> configurationFilterStrings, + ImmutableList<Density> densities, + ImmutableList<String> densityStrings) { + this.ruleContext = ruleContext; + this.configurationFilters = configurationFilters; + this.configurationFilterStrings = configurationFilterStrings; + this.densities = densities; + this.densityStrings = densityStrings; + } + + private static boolean hasAttr(RuleContext ruleContext, String attrName) { + return ruleContext.attributes().isAttributeValueExplicitlySpecified(attrName); + } + + static boolean hasFilters(RuleContext ruleContext) { + return hasAttr(ruleContext, RESOURCE_CONFIGURATION_FILTERS_NAME) + || hasAttr(ruleContext, DENSITIES_NAME); + } + + /** + * Extracts filters from the current RuleContext, as a list of strings. + * + * <p>In BUILD files, string lists can be represented as a list of strings, a single + * comma-separated string, or a combination of both. This method outputs a single list of + * individual string values, which can then be passed directly to resource processing actions. + * + * @return the values of this attribute contained in the {@link RuleContext}, as a list. + */ + private static ImmutableList<String> extractFilters(RuleContext ruleContext, String attrName) { + if (!hasAttr(ruleContext, attrName)) { + return ImmutableList.<String>of(); + } + + /* + * To deal with edge cases involving placement of whitespace and multiple strings inside a + * single item of the given list, manually build the list here rather than call something like + * {@link RuleContext#getTokenizedStringListAttr}. + * + * Filter out all empty values, even those that were explicitly provided. Paying attention to + * empty values is never helpful: even if code handled them correctly (and not all of it does) + * empty filter values result in all resources matching the empty filter, meaning that filtering + * does nothing (even if non-empty filters were also provided). + */ + List<String> rawValues = ruleContext.attributes().get(attrName, Type.STRING_LIST); + ImmutableList.Builder<String> builder = ImmutableList.builder(); + + for (String rawValue : rawValues) { + if (rawValue.contains(",")) { + for (String token : rawValue.split(",")) { + if (!token.trim().isEmpty()) { + builder.add(token.trim()); + } + } + } else if (!rawValue.isEmpty()) { + builder.add(rawValue); + } + } + + return builder.build(); + } + + static ResourceFilter fromRuleContext(RuleContext ruleContext) { + Preconditions.checkNotNull(ruleContext); + + if (!hasFilters(ruleContext)) { + return empty(ruleContext); + } + + ImmutableList<String> resourceConfigurationFilters = + extractFilters(ruleContext, RESOURCE_CONFIGURATION_FILTERS_NAME); + ImmutableList<String> densities = extractFilters(ruleContext, DENSITIES_NAME); + + boolean usePrefiltering = + ruleContext.getFragment(AndroidConfiguration.class).useResourcePrefiltering(); + + ImmutableList.Builder<FolderConfiguration> filterBuilder = ImmutableList.builder(); + if (usePrefiltering) { + for (String filter : resourceConfigurationFilters) { + addIfNotNull( + FolderConfiguration.getConfigForQualifierString(filter), + filter, + filterBuilder, + ruleContext, + RESOURCE_CONFIGURATION_FILTERS_NAME); + } + } + + ImmutableList.Builder<Density> densityBuilder = ImmutableList.builder(); + if (usePrefiltering) { + for (String density : densities) { + addIfNotNull( + Density.getEnum(density), density, densityBuilder, ruleContext, DENSITIES_NAME); + } + } + + return new ResourceFilter( + ruleContext, + filterBuilder.build(), + resourceConfigurationFilters, + densityBuilder.build(), + densities); + } + + /** Reports an attribute error if the given item is null, and otherwise adds it to the builder. */ + private static <T> void addIfNotNull( + T item, + String itemString, + ImmutableList.Builder<T> builder, + RuleContext ruleContext, + String attrName) { + if (item == null) { + ruleContext.attributeError( + attrName, "String '" + itemString + "' is not a valid value for " + attrName); + } else { + builder.add(item); + } + } + + static ResourceFilter empty(RuleContext ruleContext) { + return new ResourceFilter( + ruleContext, + ImmutableList.<FolderConfiguration>of(), + ImmutableList.<String>of(), + ImmutableList.<Density>of(), + ImmutableList.<String>of()); + } + + /** + * Filters a NestedSet of resource containers. This may be a no-op if this filter is empty or if + * resource prefiltering is disabled. + */ + NestedSet<ResourceContainer> filter(NestedSet<ResourceContainer> resources) { + if (!isPrefiltering()) { + /* + * If the filter is empty or resource prefiltering is disabled, just return the original, + * rather than make a copy. + * + * Resources should only be prefiltered in top-level android targets (such as android_binary). + * The output of resource processing, which includes the input NestedSet<ResourceContainer> + * returned by this method, is exposed to other actions via the AndroidResourcesProvider. If + * this method did a no-op copy and collapse in those cases, rather than just return the + * original NestedSet, we would lose all of the advantages around memory and time that + * NestedSets provide: each android_library target would have to copy the resources provided + * by its dependencies into a new NestedSet rather than just create a NestedSet pointing at + * its dependencies's NestedSets. + */ + return resources; + } + + NestedSetBuilder<ResourceContainer> builder = new NestedSetBuilder<>(resources.getOrder()); + + for (ResourceContainer resource : resources) { + builder.add(resource.filter(this)); + } + + return builder.build(); + } + + ImmutableList<Artifact> filter(ImmutableList<Artifact> artifacts) { + if (!isPrefiltering()) { + return artifacts; + } + + /* + * Build an ImmutableSet rather than an ImmutableList to remove duplicate Artifacts in the case + * where one Artifact is the best option for multiple densities. + */ + ImmutableSet.Builder<Artifact> builder = ImmutableSet.builder(); + + List<BestArtifactsForDensity> bestArtifactsForAllDensities = new ArrayList<>(); + for (Density density : densities) { + bestArtifactsForAllDensities.add(new BestArtifactsForDensity(density)); + } + + for (Artifact artifact : artifacts) { + FolderConfiguration configuration = getConfigForArtifact(artifact); + if (!matchesConfigurationFilters(configuration)) { + continue; + } + + if (!shouldFilterByDensity(artifact)) { + builder.add(artifact); + continue; + } + + for (BestArtifactsForDensity bestArtifactsForDensity : bestArtifactsForAllDensities) { + bestArtifactsForDensity.maybeAddArtifact(artifact); + } + } + + for (BestArtifactsForDensity bestArtifactsForDensity : bestArtifactsForAllDensities) { + builder.addAll(bestArtifactsForDensity.get()); + } + + ImmutableSet<Artifact> keptArtifacts = builder.build(); + for (Artifact artifact : artifacts) { + if (keptArtifacts.contains(artifact)) { + continue; + } + + String parentDir = artifact.getPath().getParentDirectory().getBaseName(); + filteredResources.add(parentDir + "/" + artifact.getFilename()); + } + + return keptArtifacts.asList(); + } + + /** + * Tracks the best artifact for a desired density for each combination of filename and non-density + * qualifiers. + */ + private class BestArtifactsForDensity { + private final Density desiredDensity; + + // Use a LinkedHashMap to preserve determinism. + private final Map<String, Artifact> nameAndConfigurationToBestArtifact = new LinkedHashMap<>(); + + public BestArtifactsForDensity(Density density) { + desiredDensity = density; + } + + /** + * @param artifact if this artifact is a better match for this object's desired density than any + * other artifacts with the same name and non-density configuration, adds it to this object. + */ + public void maybeAddArtifact(Artifact artifact) { + FolderConfiguration config = getConfigForArtifact(artifact); + + // We want to find a single best artifact for each combination of non-density qualifiers and + // filename. Combine those two values to create a single unique key. + config.setDensityQualifier(null); + String nameAndConfiguration = config.getUniqueKey() + "/" + artifact.getFilename(); + + Artifact currentBest = nameAndConfigurationToBestArtifact.get(nameAndConfiguration); + + if (currentBest == null || computeAffinity(artifact) < computeAffinity(currentBest)) { + nameAndConfigurationToBestArtifact.put(nameAndConfiguration, artifact); + } + } + + /** @return the collection of best Artifacts for this density. */ + public Collection<Artifact> get() { + return nameAndConfigurationToBestArtifact.values(); + } + + /** + * Compute how well this artifact matches the {@link #desiredDensity}. + * + * <p>Various different codebases have different and sometimes contradictory methods for which + * resources are better in different situations. All of them agree that an exact match is best, + * but: + * + * <p>The android common code (see {@link FolderConfiguration#getDensityQualifier()} treats + * larger densities as better than non-matching smaller densities. + * + * <p>aapt code to filter assets by density prefers the smallest density that is larger than or + * the same as the desired density, or, lacking that, the largest available density. + * + * <p>Other implementations of density filtering include Gradle (to filter which resources + * actually get built into apps) and Android code itself (for the device to decide which + * resource to use). + * + * <p>This particular implementation is based on {@link + * com.google.devtools.build.android.DensitySpecificResourceFilter}, which filters resources by + * density during execution. It prefers to use exact matches when possible, then tries to find + * resources with exactly double the desired density for particularly easy downsizing, and + * otherwise prefers resources that are closest to the desired density, relative to the smaller + * of the available and desired densities. + * + * <p>Once we always filter resources during analysis, we should be able to completely remove + * that code. + * + * @return a score for how well the artifact matches. Lower scores indicate better matches. + */ + private double computeAffinity(Artifact artifact) { + DensityQualifier resourceQualifier = getConfigForArtifact(artifact).getDensityQualifier(); + if (resourceQualifier == null) { + return Double.MAX_VALUE; + } + + int resourceDensity = resourceQualifier.getValue().getDpiValue(); + int density = desiredDensity.getDpiValue(); + + if (resourceDensity == density) { + // Exact match is the best. + return -2; + } + + if (resourceDensity == 2 * density) { + // It's very efficient to downsample an image that's exactly twice the screen + // density, so we prefer that over other non-perfect matches. + return -1; + } + + // Find the ratio between the larger and smaller of the available and desired densities. + double densityRatio = + Math.max(density, resourceDensity) / (double) Math.min(density, resourceDensity); + + if (density < resourceDensity) { + return densityRatio; + } + + // Apply a slight bias against resources that are smaller than those of the desired density. + // This becomes relevant only when we are considering multiple resources with the same ratio. + return densityRatio + 0.01; + } + } + + private FolderConfiguration getConfigForArtifact(Artifact artifact) { + String containingFolder = getContainingFolder(artifact); + FolderConfiguration config = FolderConfiguration.getConfigForFolder(containingFolder); + + if (config == null) { + ruleContext.ruleError( + "Resource folder '" + containingFolder + "' has invalid resource qualifiers"); + + return FolderConfiguration.getConfigForQualifierString(""); + } + + // aapt explicitly ignores the version qualifier; duplicate this behavior here. + config.setVersionQualifier(VersionQualifier.getQualifier("")); + + return config; + } + + /** + * Checks if we should filter this artifact by its density. + * + * <p>We filter by density if there are densities to filter by, the artifact is in a Drawable + * directory, and the artifact is not an XML file. + * + * <p>Similarly-named XML files may contain different resource definitions, so it's impossible to + * ensure that all required resources will be provided without that XML file unless we parse it. + */ + private boolean shouldFilterByDensity(Artifact artifact) { + return !densities.isEmpty() + && !artifact.getExtension().equals("xml") + && ResourceFolderType.getFolderType(getContainingFolder(artifact)) + == ResourceFolderType.DRAWABLE; + } + + private static String getContainingFolder(Artifact artifact) { + return artifact.getPath().getParentDirectory().getBaseName(); + } + + private boolean matchesConfigurationFilters(FolderConfiguration config) { + for (FolderConfiguration filter : configurationFilters) { + if (config.isMatchFor(filter)) { + return true; + } + } + + return configurationFilters.isEmpty(); + } + + /** + * Returns if this object contains a non-empty resource configuration filter. + * + * <p>Note that non-empty filters are not guaranteed to filter resources during the analysis + * phase. + */ + boolean hasConfigurationFilters() { + return !configurationFilterStrings.isEmpty(); + } + + String getConfigurationFilterString() { + return Joiner.on(',').join(configurationFilterStrings); + } + + /** + * Returns if this object contains a non-empty density filter. + * + * <p>Note that non-empty filters are not guaranteed to filter resources during the analysis + * phase. + */ + boolean hasDensities() { + return !densityStrings.isEmpty(); + } + + String getDensityString() { + return Joiner.on(',').join(densityStrings); + } + + List<String> getDensities() { + return densityStrings; + } + + boolean isPrefiltering() { + return !densities.isEmpty() || !configurationFilters.isEmpty(); + } + + /** + * TODO: Stop tracking these once android_library targets also filter resources correctly. + * + * <p>Currently, android_library targets pass do no filtering, and all resources are built into + * their symbol files. The android_binary target filters out these resources in analysis. However, + * the filtered resources must be passed to resource processing at execution time so the code + * knows to ignore resources that were filtered out. Without this, resource processing code would + * see references to those resources in dependencies's symbol files, but then be unable to follow + * those references or know whether they were missing due to resource filtering or a bug. + * + * @return a list of resources that were filtered out by this filter + */ + ImmutableList<String> getFilteredResources() { + return filteredResources.build().asList(); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceShrinkerActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceShrinkerActionBuilder.java index 517b124370..60d414ddc4 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceShrinkerActionBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceShrinkerActionBuilder.java @@ -44,7 +44,7 @@ public class ResourceShrinkerActionBuilder { private List<String> uncompressedExtensions = Collections.emptyList(); private List<String> assetsToIgnore = Collections.emptyList(); - private ResourceConfigurationFilter resourceConfigs; + private ResourceFilter resourceFilter; /** * @param ruleContext The RuleContext of the owning rule. @@ -53,7 +53,7 @@ public class ResourceShrinkerActionBuilder { this.ruleContext = ruleContext; this.spawnActionBuilder = new SpawnAction.Builder(); this.sdk = AndroidSdkProvider.fromRuleContext(ruleContext); - this.resourceConfigs = ResourceConfigurationFilter.empty(ruleContext); + this.resourceFilter = ResourceFilter.empty(ruleContext); } public ResourceShrinkerActionBuilder setUncompressedExtensions( @@ -67,12 +67,9 @@ public class ResourceShrinkerActionBuilder { return this; } - /** - * @param resourceConfigs The configuration filters to apply to the resources. - */ - public ResourceShrinkerActionBuilder setConfigurationFilters( - ResourceConfigurationFilter resourceConfigs) { - this.resourceConfigs = resourceConfigs; + /** @param resourceFilter The filters to apply to the resources. */ + public ResourceShrinkerActionBuilder setResourceFilter(ResourceFilter resourceFilter) { + this.resourceFilter = resourceFilter; return this; } @@ -149,7 +146,7 @@ public class ResourceShrinkerActionBuilder { ImmutableList.Builder<Artifact> outputs = ImmutableList.builder(); CustomCommandLine.Builder commandLine = new CustomCommandLine.Builder(); - + // Set the busybox tool. commandLine.add("--tool").add("SHRINK").add("--"); @@ -176,8 +173,8 @@ public class ResourceShrinkerActionBuilder { if (ruleContext.getConfiguration().getCompilationMode() != CompilationMode.OPT) { commandLine.add("--debug"); } - if (!resourceConfigs.isEmpty()) { - commandLine.add("--resourceConfigs").add(resourceConfigs.getFilterString()); + if (resourceFilter.hasConfigurationFilters()) { + commandLine.add("--resourceConfigs").add(resourceFilter.getConfigurationFilterString()); } checkNotNull(resourceFilesZip); diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java index 26b5eea844..62c30bc67f 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java @@ -1150,7 +1150,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "r", "android_binary(name = 'r',", " manifest = 'AndroidManifest.xml',", - " resource_configuration_filters = ['en'],", + " resource_configuration_filters = ['', 'en, es, '],", + " densities = ['hdpi, , ', 'xhdpi'],", " resource_files = ['" + Joiner.on("', '").join(resources) + "'])"); ResourceContainer directResources = getResourceContainer(binary, /* transitive= */ false); @@ -1159,6 +1160,11 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { // Validate that the input to resource processing contains all values. assertThat(resourceInputPaths(dir, directResources)).containsAllIn(resources); + + // Validate that the filters are correctly passed to the resource processing action + // This includes trimming whitespace and ignoring empty filters. + assertThat(resourceGeneratingAction(directResources).getArguments()).contains("en,es"); + assertThat(resourceGeneratingAction(directResources).getArguments()).contains("hdpi,xhdpi"); } @Test @@ -1212,6 +1218,14 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { } @Test + public void testFilteredResourcesMultipleFiltersWithWhitespace() throws Exception { + testDirectResourceFiltering( + " en , es ", + /* unexpectedQualifiers= */ ImmutableList.of("fr"), + /* expectedQualifiers= */ ImmutableList.of("en", "es")); + } + + @Test public void testFilteredResourcesMultipleFilterStrings() throws Exception { testDirectResourceFiltering( "en', 'es", @@ -1313,20 +1327,165 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { /* expectedQualifiers= */ ImmutableList.of(filters)); } + @Test + public void testDensityFilteredResourcesSingleDensity() throws Exception { + testDensityResourceFiltering( + "hdpi", ImmutableList.of("ldpi", "mdpi", "xhdpi"), ImmutableList.of("hdpi")); + } + + @Test + public void testDensityFilteredResourcesSingleClosestDensity() throws Exception { + testDensityResourceFiltering( + "hdpi", ImmutableList.of("ldpi", "mdpi"), ImmutableList.of("xhdpi")); + } + + @Test + public void testDensityFilteredResourcesDifferingQualifiers() throws Exception { + testDensityResourceFiltering( + "hdpi", + ImmutableList.of("en-xhdpi", "fr"), + ImmutableList.of("en-hdpi", "fr-mdpi", "xhdpi")); + } + + @Test + public void testDensityFilteredResourcesMultipleDensities() throws Exception { + testDensityResourceFiltering( + "ldpi,hdpi', 'xhdpi", + ImmutableList.of("mdpi", "xxhdpi"), + ImmutableList.of("ldpi", "hdpi", "xhdpi")); + } + + @Test + public void testDensityFilteredResourcesDoubledDensity() throws Exception { + testDensityResourceFiltering( + "hdpi", ImmutableList.of("ldpi", "mdpi", "xhdpi"), ImmutableList.of("xxhdpi")); + } + + @Test + public void testDensityFilteredResourcesDifferentRatiosSmallerCloser() throws Exception { + testDensityResourceFiltering("mdpi", ImmutableList.of("hdpi"), ImmutableList.of("ldpi")); + } + + @Test + public void testDensityFilteredResourcesDifferentRatiosLargerCloser() throws Exception { + testDensityResourceFiltering("hdpi", ImmutableList.of("mdpi"), ImmutableList.of("xhdpi")); + } + + @Test + public void testDensityFilteredResourcesSameRatioPreferLarger() throws Exception { + // xxhdpi is 480dpi and xxxhdpi is 640dpi. + // The ratios between 360dpi and 480dpi and between 480dpi and 640dpi are both 3:4. + testDensityResourceFiltering("xxhdpi", ImmutableList.of("360dpi"), ImmutableList.of("xxxhdpi")); + } + + @Test + public void testDensityFilteredResourcesOptionsSmallerThanDesired() throws Exception { + testDensityResourceFiltering( + "xxxhdpi", ImmutableList.of("xhdpi", "mdpi", "ldpi"), ImmutableList.of("xxhdpi")); + } + + @Test + public void testDensityFilteredResourcesOptionsLargerThanDesired() throws Exception { + testDensityResourceFiltering( + "ldpi", ImmutableList.of("xxxhdpi", "xxhdpi", "xhdpi"), ImmutableList.of("mdpi")); + } + + @Test + public void testDensityFilteredResourcesSpecialValues() throws Exception { + testDensityResourceFiltering( + "xxxhdpi", ImmutableList.of("anydpi", "nodpi"), ImmutableList.of("ldpi")); + } + + @Test + public void testDensityFilteredResourcesXml() throws Exception { + testDirectResourceFiltering( + /* resourceConfigurationFilters= */ "", + "hdpi", + ImmutableList.<String>of(), + ImmutableList.of("ldpi", "mdpi", "hdpi", "xhdpi"), + /* expectUnqualifiedResource= */ true, + "drawable", + "xml"); + } + + @Test + public void testDensityFilteredResourcesNotDrawable() throws Exception { + testDirectResourceFiltering( + /* resourceConfigurationFilters= */ "", + "hdpi", + ImmutableList.<String>of(), + ImmutableList.of("ldpi", "mdpi", "hdpi", "xhdpi"), + /* expectUnqualifiedResource= */ true, + "layout", + "png"); + } + + @Test + public void testQualifierAndDensityFilteredResources() throws Exception { + testDirectResourceFiltering( + "en,fr-mdpi", + "ldpi,hdpi", + ImmutableList.of("mdpi", "es-ldpi", "en-xxxhdpi", "fr-mdpi"), + ImmutableList.of("ldpi", "hdpi", "en-xhdpi", "fr-hdpi"), + /* expectUnqualifiedResource= */ false, + "drawable", + "png"); + } + + private void testDirectResourceFiltering( + String filters, List<String> unexpectedQualifiers, ImmutableList<String> expectedQualifiers) + throws Exception { + testDirectResourceFiltering( + filters, + /* densities= */ "", + unexpectedQualifiers, + expectedQualifiers, + /* expectUnqualifiedResource= */ true, + "drawable", + "png"); + } + + private void testDensityResourceFiltering( + String densities, List<String> unexpectedQualifiers, List<String> expectedQualifiers) + throws Exception { + testDirectResourceFiltering( + /* resourceConfigurationFilters= */ "", + densities, + unexpectedQualifiers, + expectedQualifiers, + /* expectUnqualifiedResource= */ false, + "drawable", + "png"); + } + private void testDirectResourceFiltering( String resourceConfigurationFilters, + String densities, List<String> unexpectedQualifiers, - List<String> expectedQualifiers) + List<String> expectedQualifiers, + boolean expectUnqualifiedResource, + String folderType, + String suffix) throws Exception { List<String> unexpectedResources = new ArrayList<>(); + List<String> expectedFiltered = new ArrayList<>(); for (String qualifier : unexpectedQualifiers) { - unexpectedResources.add("res/values-" + qualifier + "/foo.xml"); + expectedFiltered.add(folderType + "-" + qualifier + "/foo." + suffix); + unexpectedResources.add("res/" + folderType + "-" + qualifier + "/foo." + suffix); } List<String> expectedResources = new ArrayList<>(); for (String qualifier : expectedQualifiers) { - expectedResources.add("res/values-" + qualifier + "/foo.xml"); + expectedResources.add("res/" + folderType + "-" + qualifier + "/foo." + suffix); + } + + String unqualifiedResource = "res/" + folderType + "/foo." + suffix; + if (expectUnqualifiedResource) { + expectedResources.add(unqualifiedResource); + } else { + unexpectedResources.add(unqualifiedResource); + expectedFiltered.add(folderType + "/foo." + suffix); } // Default resources should never be filtered @@ -1345,6 +1504,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "android_binary(name = 'r',", " manifest = 'AndroidManifest.xml',", " resource_configuration_filters = ['" + resourceConfigurationFilters + "'],", + " densities = ['" + densities + "'],", " resource_files = ['" + Joiner.on("', '").join(allResources) + "'])"); ResourceContainer directResources = getResourceContainer(binary, /* transitive= */ false); @@ -1356,6 +1516,21 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { // Validate that the input to resource processing contains only the filtered values. assertThat(resourceInputPaths(dir, directResources)).containsAllIn(expectedResources); assertThat(resourceInputPaths(dir, directResources)).containsNoneIn(unexpectedResources); + + if (expectedFiltered.isEmpty()) { + assertThat(resourceArguments(directResources)).doesNotContain("--prefilteredResources"); + } else { + String[] flagValues = + flagValue("--prefilteredResources", resourceArguments(directResources)).split(","); + assertThat(flagValues).asList().containsAllIn(expectedFiltered); + assertThat(flagValues).hasLength(expectedFiltered.size()); + } + + // Validate resource filters are not passed to execution, since they were applied in analysis + assertThat(resourceGeneratingAction(directResources).getArguments()) + .doesNotContain(ResourceFilter.RESOURCE_CONFIGURATION_FILTERS_NAME); + assertThat(resourceGeneratingAction(directResources).getArguments()) + .doesNotContain(ResourceFilter.DENSITIES_NAME); } @Test |