| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * 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.android.tools.lint.checks; |
| |
| import static com.android.SdkConstants.ANDROID_PREFIX; |
| import static com.android.SdkConstants.ATTR_LOCALE; |
| import static com.android.SdkConstants.ATTR_NAME; |
| import static com.android.SdkConstants.ATTR_TRANSLATABLE; |
| import static com.android.SdkConstants.FD_RES_VALUES; |
| import static com.android.SdkConstants.STRING_PREFIX; |
| import static com.android.SdkConstants.TAG_ITEM; |
| import static com.android.SdkConstants.TAG_STRING; |
| import static com.android.SdkConstants.TAG_STRING_ARRAY; |
| import static com.android.SdkConstants.TOOLS_URI; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.builder.model.AndroidProject; |
| import com.android.builder.model.ProductFlavor; |
| import com.android.builder.model.ProductFlavorContainer; |
| import com.android.builder.model.Variant; |
| import com.android.ide.common.resources.LocaleManager; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.ide.common.resources.configuration.LocaleQualifier; |
| import com.android.resources.ResourceFolderType; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.Project; |
| import com.android.tools.lint.detector.api.ResourceXmlDetector; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.XmlContext; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Checks for incomplete translations - e.g. keys that are only present in some |
| * locales but not all. |
| */ |
| public class TranslationDetector extends ResourceXmlDetector { |
| @VisibleForTesting |
| static boolean sCompleteRegions = |
| System.getenv("ANDROID_LINT_COMPLETE_REGIONS") != null; //$NON-NLS-1$ |
| |
| private static final Implementation IMPLEMENTATION = new Implementation( |
| TranslationDetector.class, |
| Scope.ALL_RESOURCES_SCOPE); |
| |
| /** Are all translations complete? */ |
| public static final Issue MISSING = Issue.create( |
| "MissingTranslation", //$NON-NLS-1$ |
| "Incomplete translation", |
| "If an application has more than one locale, then all the strings declared in " + |
| "one language should also be translated in all other languages.\n" + |
| "\n" + |
| "If the string should *not* be translated, you can add the attribute " + |
| "`translatable=\"false\"` on the `<string>` element, or you can define all " + |
| "your non-translatable strings in a resource file called `donottranslate.xml`. " + |
| "Or, you can ignore the issue with a `tools:ignore=\"MissingTranslation\"` " + |
| "attribute.\n" + |
| "\n" + |
| "By default this detector allows regions of a language to just provide a " + |
| "subset of the strings and fall back to the standard language strings. " + |
| "You can require all regions to provide a full translation by setting the " + |
| "environment variable `ANDROID_LINT_COMPLETE_REGIONS`.\n" + |
| "\n" + |
| "You can tell lint (and other tools) which language is the default language " + |
| "in your `res/values/` folder by specifying `tools:locale=\"languageCode\"` for " + |
| "the root `<resources>` element in your resource file. (The `tools` prefix refers " + |
| "to the namespace declaration `http://schemas.android.com/tools`.)", |
| Category.MESSAGES, |
| 8, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| /** Are there extra translations that are "unused" (appear only in specific languages) ? */ |
| public static final Issue EXTRA = Issue.create( |
| "ExtraTranslation", //$NON-NLS-1$ |
| "Extra translation", |
| "If a string appears in a specific language translation file, but there is " + |
| "no corresponding string in the default locale, then this string is probably " + |
| "unused. (It's technically possible that your application is only intended to " + |
| "run in a specific locale, but it's still a good idea to provide a fallback.).\n" + |
| "\n" + |
| "Note that these strings can lead to crashes if the string is looked up on any " + |
| "locale not providing a translation, so it's important to clean them up.", |
| Category.MESSAGES, |
| 6, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| private Set<String> mNames; |
| private Set<String> mTranslatedArrays; |
| private Set<String> mNonTranslatable; |
| private boolean mIgnoreFile; |
| private Map<File, Set<String>> mFileToNames; |
| private Map<File, String> mFileToLocale; |
| |
| /** Locations for each untranslated string name. Populated during phase 2, if necessary */ |
| private Map<String, Location> mMissingLocations; |
| |
| /** Locations for each extra translated string name. Populated during phase 2, if necessary */ |
| private Map<String, Location> mExtraLocations; |
| |
| /** Error messages for each untranslated string name. Populated during phase 2, if necessary */ |
| private Map<String, String> mDescriptions; |
| |
| /** Constructs a new {@link TranslationDetector} */ |
| public TranslationDetector() { |
| } |
| |
| @Override |
| public boolean appliesTo(@NonNull ResourceFolderType folderType) { |
| return folderType == ResourceFolderType.VALUES; |
| } |
| |
| @Override |
| public Collection<String> getApplicableElements() { |
| return Arrays.asList( |
| TAG_STRING, |
| TAG_STRING_ARRAY |
| ); |
| } |
| |
| @Override |
| public void beforeCheckProject(@NonNull Context context) { |
| if (context.getDriver().getPhase() == 1) { |
| mFileToNames = new HashMap<File, Set<String>>(); |
| } |
| } |
| |
| @Override |
| public void beforeCheckFile(@NonNull Context context) { |
| if (context.getPhase() == 1) { |
| mNames = new HashSet<String>(); |
| } |
| |
| // Convention seen in various projects |
| mIgnoreFile = context.file.getName().startsWith("donottranslate") //$NON-NLS-1$ |
| || UnusedResourceDetector.isAnalyticsFile(context); |
| |
| if (!context.getProject().getReportIssues()) { |
| mIgnoreFile = true; |
| } |
| } |
| |
| @Override |
| public void afterCheckFile(@NonNull Context context) { |
| if (context.getPhase() == 1) { |
| // Store this layout's set of ids for full project analysis in afterCheckProject |
| if (context.getProject().getReportIssues() && mNames != null && !mNames.isEmpty()) { |
| mFileToNames.put(context.file, mNames); |
| |
| Element root = ((XmlContext) context).document.getDocumentElement(); |
| if (root != null) { |
| String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE); |
| if (locale != null && !locale.isEmpty()) { |
| if (mFileToLocale == null) { |
| mFileToLocale = Maps.newHashMap(); |
| } |
| mFileToLocale.put(context.file, locale); |
| } |
| // Add in English here if not specified? Worry about false positives listing "en" explicitly |
| } |
| } |
| |
| mNames = null; |
| } |
| } |
| |
| @Override |
| public void afterCheckProject(@NonNull Context context) { |
| if (context.getPhase() == 1) { |
| // NOTE - this will look for the presence of translation strings. |
| // If you create a resource folder but don't actually place a file in it |
| // we won't detect that, but it seems like a smaller problem. |
| |
| checkTranslations(context); |
| |
| mFileToNames = null; |
| |
| if (mMissingLocations != null || mExtraLocations != null) { |
| context.getDriver().requestRepeat(this, Scope.ALL_RESOURCES_SCOPE); |
| } |
| } else { |
| assert context.getPhase() == 2; |
| |
| reportMap(context, MISSING, mMissingLocations); |
| reportMap(context, EXTRA, mExtraLocations); |
| mMissingLocations = null; |
| mExtraLocations = null; |
| mDescriptions = null; |
| } |
| } |
| |
| private void reportMap(Context context, Issue issue, Map<String, Location> map) { |
| if (map != null) { |
| for (Map.Entry<String, Location> entry : map.entrySet()) { |
| Location location = entry.getValue(); |
| String name = entry.getKey(); |
| String message = mDescriptions.get(name); |
| |
| if (location == null) { |
| location = Location.create(context.getProject().getDir()); |
| } |
| |
| // We were prepending locations, but we want to prefer the base folders |
| location = Location.reverse(location); |
| |
| context.report(issue, location, message); |
| } |
| } |
| } |
| |
| private void checkTranslations(Context context) { |
| // Only one file defining strings? If so, no problems. |
| Set<File> files = mFileToNames.keySet(); |
| Set<File> parentFolders = new HashSet<File>(); |
| for (File file : files) { |
| parentFolders.add(file.getParentFile()); |
| } |
| if (parentFolders.size() == 1 |
| && FD_RES_VALUES.equals(parentFolders.iterator().next().getName())) { |
| // Only one language - no problems. |
| return; |
| } |
| |
| boolean reportMissing = context.isEnabled(MISSING); |
| boolean reportExtra = context.isEnabled(EXTRA); |
| |
| // res/strings.xml etc |
| String defaultLanguage = "Default"; |
| |
| Map<File, String> parentFolderToLanguage = new HashMap<File, String>(); |
| for (File parent : parentFolders) { |
| String name = parent.getName(); |
| |
| // Look up the language for this folder. |
| String language = getLanguageTag(name); |
| if (language == null) { |
| language = defaultLanguage; |
| } |
| |
| parentFolderToLanguage.put(parent, language); |
| } |
| |
| int languageCount = parentFolderToLanguage.values().size(); |
| if (languageCount == 0 || languageCount == 1 && defaultLanguage.equals( |
| parentFolderToLanguage.values().iterator().next())) { |
| // At most one language -- no problems. |
| return; |
| } |
| |
| // Merge together the various files building up the translations for each language |
| Map<String, Set<String>> languageToStrings = |
| new HashMap<String, Set<String>>(languageCount); |
| Set<String> allStrings = new HashSet<String>(200); |
| for (File file : files) { |
| String language = null; |
| if (mFileToLocale != null) { |
| String locale = mFileToLocale.get(file); |
| if (locale != null) { |
| int index = locale.indexOf('-'); |
| if (index != -1) { |
| locale = locale.substring(0, index); |
| } |
| language = locale; |
| } |
| } |
| if (language == null) { |
| language = parentFolderToLanguage.get(file.getParentFile()); |
| } |
| assert language != null : file.getParent(); |
| Set<String> fileStrings = mFileToNames.get(file); |
| |
| Set<String> languageStrings = languageToStrings.get(language); |
| if (languageStrings == null) { |
| // We don't need a copy; we're done with the string tables now so we |
| // can modify them |
| languageToStrings.put(language, fileStrings); |
| } else { |
| languageStrings.addAll(fileStrings); |
| } |
| allStrings.addAll(fileStrings); |
| } |
| |
| Set<String> defaultStrings = languageToStrings.get(defaultLanguage); |
| if (defaultStrings == null) { |
| defaultStrings = new HashSet<String>(); |
| } |
| |
| // See if it looks like the user has named a specific locale as the base language |
| // (this impacts whether we report strings as "extra" or "missing") |
| if (mFileToLocale != null) { |
| Set<String> specifiedLocales = Sets.newHashSet(); |
| for (Map.Entry<File, String> entry : mFileToLocale.entrySet()) { |
| String locale = entry.getValue(); |
| int index = locale.indexOf('-'); |
| if (index != -1) { |
| locale = locale.substring(0, index); |
| } |
| specifiedLocales.add(locale); |
| } |
| if (specifiedLocales.size() == 1) { |
| String first = specifiedLocales.iterator().next(); |
| Set<String> languageStrings = languageToStrings.get(first); |
| assert languageStrings != null; |
| defaultStrings.addAll(languageStrings); |
| } |
| } |
| |
| int stringCount = allStrings.size(); |
| |
| // Treat English is the default language if not explicitly specified |
| if (!sCompleteRegions && !languageToStrings.containsKey("en") |
| && mFileToLocale == null) { //$NON-NLS-1$ |
| // But only if we have an actual region |
| for (String l : languageToStrings.keySet()) { |
| if (l.startsWith("en-")) { //$NON-NLS-1$ |
| languageToStrings.put("en", defaultStrings); //$NON-NLS-1$ |
| break; |
| } |
| } |
| } |
| |
| List<String> resConfigLanguages = getResConfigLanguages(context.getMainProject()); |
| if (resConfigLanguages != null) { |
| List<String> keys = Lists.newArrayList(languageToStrings.keySet()); |
| for (String locale : keys) { |
| if (defaultLanguage.equals(locale)) { |
| continue; |
| } |
| String language = locale; |
| int index = language.indexOf('-'); |
| if (index != -1) { |
| // Strip off region |
| language = language.substring(0, index); |
| } |
| if (!resConfigLanguages.contains(language)) { |
| languageToStrings.remove(locale); |
| } |
| } |
| } |
| |
| // Do we need to resolve fallback strings for regions that only define a subset |
| // of the strings in the language and fall back on the main language for the rest? |
| if (!sCompleteRegions) { |
| for (String l : languageToStrings.keySet()) { |
| if (l.indexOf('-') != -1) { |
| // Yes, we have regions. Merge all base language string names into each region. |
| for (Map.Entry<String, Set<String>> entry : languageToStrings.entrySet()) { |
| Set<String> strings = entry.getValue(); |
| if (stringCount != strings.size()) { |
| String languageRegion = entry.getKey(); |
| int regionIndex = languageRegion.indexOf('-'); |
| if (regionIndex != -1) { |
| String language = languageRegion.substring(0, regionIndex); |
| Set<String> fallback = languageToStrings.get(language); |
| if (fallback != null) { |
| strings.addAll(fallback); |
| } |
| } |
| } |
| } |
| // We only need to do this once; when we see the first region we know |
| // we need to do it; once merged we can bail |
| break; |
| } |
| } |
| } |
| |
| // Fast check to see if there's no problem: if the default locale set is the |
| // same as the all set (meaning there are no extra strings in the other languages) |
| // then we can quickly determine if everything is okay by just making sure that |
| // each language defines everything. If that's the case they will all have the same |
| // string count. |
| if (stringCount == defaultStrings.size()) { |
| boolean haveError = false; |
| for (Map.Entry<String, Set<String>> entry : languageToStrings.entrySet()) { |
| Set<String> strings = entry.getValue(); |
| if (stringCount != strings.size()) { |
| haveError = true; |
| break; |
| } |
| } |
| if (!haveError) { |
| return; |
| } |
| } |
| |
| List<String> languages = new ArrayList<String>(languageToStrings.keySet()); |
| Collections.sort(languages); |
| for (String language : languages) { |
| Set<String> strings = languageToStrings.get(language); |
| if (defaultLanguage.equals(language)) { |
| continue; |
| } |
| |
| // if strings.size() == stringCount, then this language is defining everything, |
| // both all the default language strings and the union of all extra strings |
| // defined in other languages, so there's no problem. |
| if (stringCount != strings.size()) { |
| if (reportMissing) { |
| Set<String> difference = Sets.difference(defaultStrings, strings); |
| if (!difference.isEmpty()) { |
| if (mMissingLocations == null) { |
| mMissingLocations = new HashMap<String, Location>(); |
| } |
| if (mDescriptions == null) { |
| mDescriptions = new HashMap<String, String>(); |
| } |
| |
| for (String s : difference) { |
| mMissingLocations.put(s, null); |
| String message = mDescriptions.get(s); |
| if (message == null) { |
| message = String.format("\"`%1$s`\" is not translated in %2$s", |
| s, getLanguageDescription(language)); |
| } else { |
| message = message + ", " + getLanguageDescription(language); |
| } |
| mDescriptions.put(s, message); |
| } |
| } |
| } |
| } |
| |
| if (stringCount != defaultStrings.size()) { |
| if (reportExtra) { |
| Set<String> difference = Sets.difference(strings, defaultStrings); |
| if (!difference.isEmpty()) { |
| if (mExtraLocations == null) { |
| mExtraLocations = new HashMap<String, Location>(); |
| } |
| if (mDescriptions == null) { |
| mDescriptions = new HashMap<String, String>(); |
| } |
| |
| for (String s : difference) { |
| if (mTranslatedArrays != null && mTranslatedArrays.contains(s)) { |
| continue; |
| } |
| if (mNonTranslatable != null && mNonTranslatable.contains(s)) { |
| continue; |
| } |
| |
| mExtraLocations.put(s, null); |
| String message = String.format( |
| "\"`%1$s`\" is translated here but not found in default locale", s); |
| mDescriptions.put(s, message); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| public static String getLanguageDescription(@NonNull String locale) { |
| int index = locale.indexOf('-'); |
| String regionCode = null; |
| String languageCode = locale; |
| if (index != -1) { |
| regionCode = locale.substring(index + 1).toUpperCase(Locale.US); |
| languageCode = locale.substring(0, index).toLowerCase(Locale.US); |
| } |
| |
| String languageName = LocaleManager.getLanguageName(languageCode); |
| if (languageName != null) { |
| if (regionCode != null) { |
| String regionName = LocaleManager.getRegionName(regionCode); |
| if (regionName != null) { |
| languageName = languageName + ": " + regionName; |
| } |
| } |
| |
| return String.format("\"%1$s\" (%2$s)", locale, languageName); |
| } else { |
| return '"' + locale + '"'; |
| } |
| } |
| |
| |
| /** Look up the language for the given folder name */ |
| private static String getLanguageTag(String name) { |
| if (FD_RES_VALUES.equals(name)) { |
| return null; |
| } |
| |
| FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name); |
| if (configuration != null) { |
| LocaleQualifier locale = configuration.getLocaleQualifier(); |
| if (locale != null && !locale.hasFakeValue()) { |
| return locale.getTag(); |
| } |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public void visitElement(@NonNull XmlContext context, @NonNull Element element) { |
| if (mIgnoreFile) { |
| return; |
| } |
| |
| Attr attribute = element.getAttributeNode(ATTR_NAME); |
| |
| if (context.getPhase() == 2) { |
| // Just locating names requested in the {@link #mLocations} map |
| if (attribute == null) { |
| return; |
| } |
| String name = attribute.getValue(); |
| if (mMissingLocations != null && mMissingLocations.containsKey(name)) { |
| String language = getLanguageTag(context.file.getParentFile().getName()); |
| if (language == null) { |
| if (context.getDriver().isSuppressed(context, MISSING, element)) { |
| mMissingLocations.remove(name); |
| return; |
| } |
| |
| Location location = context.getLocation(attribute); |
| location.setClientData(element); |
| location.setSecondary(mMissingLocations.get(name)); |
| mMissingLocations.put(name, location); |
| } |
| } |
| if (mExtraLocations != null && mExtraLocations.containsKey(name)) { |
| String language = getLanguageTag(context.file.getParentFile().getName()); |
| if (language != null) { |
| if (context.getDriver().isSuppressed(context, EXTRA, element)) { |
| mExtraLocations.remove(name); |
| return; |
| } |
| Location location = context.getLocation(attribute); |
| location.setClientData(element); |
| location.setMessage("Also translated here"); |
| location.setSecondary(mExtraLocations.get(name)); |
| mExtraLocations.put(name, location); |
| } |
| } |
| return; |
| } |
| |
| assert context.getPhase() == 1; |
| if (attribute == null || attribute.getValue().isEmpty()) { |
| context.report(MISSING, element, context.getLocation(element), |
| "Missing `name` attribute in `<string>` declaration"); |
| } else { |
| String name = attribute.getValue(); |
| |
| Attr translatable = element.getAttributeNode(ATTR_TRANSLATABLE); |
| if (translatable != null && !Boolean.valueOf(translatable.getValue())) { |
| String l = LintUtils.getLocaleAndRegion(context.file.getParentFile().getName()); |
| //noinspection VariableNotUsedInsideIf |
| if (l != null) { |
| context.report(EXTRA, translatable, context.getLocation(translatable), |
| "Non-translatable resources should only be defined in the base " + |
| "`values/` folder"); |
| } else { |
| if (mNonTranslatable == null) { |
| mNonTranslatable = new HashSet<String>(); |
| } |
| mNonTranslatable.add(name); |
| } |
| return; |
| } else if (name.equals("google_maps_key") //$NON-NLS-1$ |
| || name.equals("google_maps_key_instructions")) { //$NON-NLS-1$ |
| // Older versions of the templates shipped with these not marked as |
| // non-translatable; don't flag them |
| if (mNonTranslatable == null) { |
| mNonTranslatable = new HashSet<String>(); |
| } |
| mNonTranslatable.add(name); |
| return; |
| } |
| |
| if (element.getTagName().equals(TAG_STRING_ARRAY) && |
| allItemsAreReferences(element)) { |
| // No need to provide translations for string arrays where all |
| // the children items are defined as translated string resources, |
| // e.g. |
| // <string-array name="foo"> |
| // <item>@string/item1</item> |
| // <item>@string/item2</item> |
| // </string-array> |
| // However, we need to remember these names such that we don't consider |
| // these arrays "extra" if one of the *translated* versions of the array |
| // perform an inline translation of an array item |
| if (mTranslatedArrays == null) { |
| mTranslatedArrays = new HashSet<String>(); |
| } |
| mTranslatedArrays.add(name); |
| return; |
| } |
| |
| // Check for duplicate name definitions? No, because there can be |
| // additional customizations like product= |
| //if (mNames.contains(name)) { |
| // context.mClient.report(ISSUE, context.getLocation(attribute), |
| // String.format("Duplicate name %1$s, already defined earlier in this file", |
| // name)); |
| //} |
| |
| mNames.add(name); |
| |
| if (mNonTranslatable != null && mNonTranslatable.contains(name)) { |
| String message = String.format("The resource string \"`%1$s`\" has been marked as " + |
| "`translatable=\"false\"`", name); |
| context.report(EXTRA, attribute, context.getLocation(attribute), message); |
| } |
| |
| // TBD: Also make sure that the strings are not empty or placeholders? |
| } |
| } |
| |
| private static boolean allItemsAreReferences(Element element) { |
| assert element.getTagName().equals(TAG_STRING_ARRAY); |
| NodeList childNodes = element.getChildNodes(); |
| for (int i = 0, n = childNodes.getLength(); i < n; i++) { |
| Node item = childNodes.item(i); |
| if (item.getNodeType() == Node.ELEMENT_NODE && |
| TAG_ITEM.equals(item.getNodeName())) { |
| NodeList itemChildren = item.getChildNodes(); |
| for (int j = 0, m = itemChildren.getLength(); j < m; j++) { |
| Node valueNode = itemChildren.item(j); |
| if (valueNode.getNodeType() == Node.TEXT_NODE) { |
| String value = valueNode.getNodeValue().trim(); |
| if (!value.startsWith(ANDROID_PREFIX) |
| && !value.startsWith(STRING_PREFIX)) { |
| return false; |
| } |
| } |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| @Nullable |
| private static List<String> getResConfigLanguages(@NonNull Project project) { |
| if (project.isGradleProject() && project.getGradleProjectModel() != null && |
| project.getCurrentVariant() != null) { |
| Set<String> relevantDensities = Sets.newHashSet(); |
| Variant variant = project.getCurrentVariant(); |
| List<String> variantFlavors = variant.getProductFlavors(); |
| AndroidProject gradleProjectModel = project.getGradleProjectModel(); |
| |
| addResConfigsFromFlavor(relevantDensities, null, |
| project.getGradleProjectModel().getDefaultConfig()); |
| for (ProductFlavorContainer container : gradleProjectModel.getProductFlavors()) { |
| addResConfigsFromFlavor(relevantDensities, variantFlavors, container); |
| } |
| if (!relevantDensities.isEmpty()) { |
| ArrayList<String> strings = Lists.newArrayList(relevantDensities); |
| Collections.sort(strings); |
| return strings; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Adds in the resConfig values specified by the given flavor container, assuming |
| * it's in one of the relevant variantFlavors, into the given set |
| */ |
| private static void addResConfigsFromFlavor(@NonNull Set<String> relevantLanguages, |
| @Nullable List<String> variantFlavors, |
| @NonNull ProductFlavorContainer container) { |
| ProductFlavor flavor = container.getProductFlavor(); |
| if (variantFlavors == null || variantFlavors.contains(flavor.getName())) { |
| if (!flavor.getResourceConfigurations().isEmpty()) { |
| for (String resConfig : flavor.getResourceConfigurations()) { |
| // Look for languages; these are of length 2. (ResConfigs |
| // can also refer to densities, etc.) |
| if (resConfig.length() == 2) { |
| relevantLanguages.add(resConfig); |
| } |
| } |
| } |
| } |
| } |
| } |