All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.resources.aar.FrameworkResourceRepository Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2018 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.resources.aar;

import static com.android.SdkConstants.DOT_9PNG;
import static com.android.SdkConstants.FD_RES_RAW;

import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.ide.common.util.PathString;
import com.android.io.CancellableFileIo;
import com.android.resources.ResourceType;
import com.android.resources.base.BasicResourceItem;
import com.android.resources.base.BasicResourceItemBase;
import com.android.resources.base.BasicValueResourceItemBase;
import com.android.resources.base.NamespaceResolver;
import com.android.resources.base.RepositoryConfiguration;
import com.android.resources.base.RepositoryLoader;
import com.android.resources.base.ResourceSerializationUtil;
import com.android.utils.Base128InputStream;
import com.android.utils.Base128OutputStream;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

/**
 * Repository of resources of the Android framework. Most client code should use
 * the ResourceRepositoryManager.getFrameworkResources method to obtain framework resources.
 *
 * 

The repository can be loaded either from a res directory containing XML files, or from * framework_res.jar file, or from a binary cache file located under the directory returned by * the {@link PathManager#getSystemPath()} method. This binary cache file can be created as * a side effect of loading the repository from a res directory. * *

Loading from framework_res.jar or a binary cache file is 3-4 times faster than loading * from res directory. * * @see FrameworkResJarCreator */ public final class FrameworkResourceRepository extends AarSourceResourceRepository { private static final ResourceNamespace ANDROID_NAMESPACE = ResourceNamespace.ANDROID; /** Mapping from languages to language groups, e.g. Romansh is mapped to Italian. */ private static final Map LANGUAGE_TO_GROUP = ImmutableMap.of("rm", "it"); private static final String RESOURCES_TABLE_PREFIX = "resources_"; private static final String RESOURCE_TABLE_SUFFIX = ".bin"; private static final String COMPILED_9PNG_EXTENSION = ".compiled.9.png"; private static final Logger LOG = Logger.getInstance(FrameworkResourceRepository.class); private final Set myLanguageGroups = new TreeSet<>(); private int myNumberOfLanguageGroupsLoadedFromCache; private final boolean myUseCompiled9Patches; private FrameworkResourceRepository(@NotNull RepositoryLoader loader, boolean useCompiled9Patches) { super(loader, null); myUseCompiled9Patches = useCompiled9Patches; } /** * Creates an Android framework resource repository. * * @param resourceDirectoryOrFile the res directory or a jar file containing resources of the Android framework * @param languagesToLoad the set of ISO 639 language codes, or null to load all available languages * @param cachingData data used to validate and create a persistent cache file * @param useCompiled9Patches whether to provide the compiled or non-compiled version of the framework 9-patches * @return the created resource repository */ @NotNull public static FrameworkResourceRepository create(@NotNull Path resourceDirectoryOrFile, @Nullable Set languagesToLoad, @Nullable CachingData cachingData, boolean useCompiled9Patches) { long start = LOG.isDebugEnabled() ? System.currentTimeMillis() : 0; Set languageGroups = languagesToLoad == null ? null : getLanguageGroups(languagesToLoad); Loader loader = new Loader(resourceDirectoryOrFile, languageGroups); FrameworkResourceRepository repository = new FrameworkResourceRepository(loader, useCompiled9Patches); repository.load(null, cachingData, loader, languageGroups, loader.myLoadedLanguageGroups); if (LOG.isDebugEnabled()) { String source = repository.getNumberOfLanguageGroupsLoadedFromOrigin() == 0 ? "cache" : repository.myNumberOfLanguageGroupsLoadedFromCache == 0 ? resourceDirectoryOrFile.toString() : "cache and " + resourceDirectoryOrFile; LOG.debug("Loaded from " + source + " with " + (repository.myLanguageGroups.size() - 1) + " languages in " + (System.currentTimeMillis() - start) / 1000. + " sec"); } return repository; } /** * Checks if the repository contains resources for the given set of languages. * * @param languages the set of ISO 639 language codes to check * @return true if the repository contains resources for all requested languages */ public boolean containsLanguages(@NotNull Set languages) { for (String language : languages) { if (!myLanguageGroups.contains(getLanguageGroup(language))) { return false; } } return true; } /** * Loads resources for requested languages that are not present in this resource repository. * * @param languagesToLoad the set of ISO 639 language codes, or null to load all available languages * @param cachingData data used to validate and create a persistent cache file * @return the new resource repository with additional resources, or this resource repository if it already contained * all requested languages */ @NotNull public FrameworkResourceRepository loadMissingLanguages(@Nullable Set languagesToLoad, @Nullable CachingData cachingData) { @Nullable Set languageGroups = languagesToLoad == null ? null : getLanguageGroups(languagesToLoad); if (languageGroups != null && myLanguageGroups.containsAll(languageGroups)) { return this; // The repository already contains all requested languages. } long start = LOG.isDebugEnabled() ? System.currentTimeMillis() : 0; Loader loader = new Loader(this, languageGroups); FrameworkResourceRepository newRepository = new FrameworkResourceRepository(loader, myUseCompiled9Patches); newRepository.load(this, cachingData, loader, languageGroups, loader.myLoadedLanguageGroups); if (LOG.isDebugEnabled()) { String source = newRepository.getNumberOfLanguageGroupsLoadedFromOrigin() == getNumberOfLanguageGroupsLoadedFromOrigin() ? "cache" : newRepository.myNumberOfLanguageGroupsLoadedFromCache == myNumberOfLanguageGroupsLoadedFromCache ? myResourceDirectoryOrFile.toString() : "cache and " + myResourceDirectoryOrFile; LOG.debug("Loaded " + (newRepository.myLanguageGroups.size() - myLanguageGroups.size()) + " additional languages from " + source + " in " + (System.currentTimeMillis() - start) / 1000. + " sec"); } return newRepository; } private void load(@Nullable FrameworkResourceRepository sourceRepository, @Nullable CachingData cachingData, @NotNull Loader loader, @Nullable Set languageGroups, @NotNull Set languageGroupsLoadedFromSourceRepositoryOrCache) { Map stringCache = Maps.newHashMapWithExpectedSize(10000); Map namespaceResolverCache = new HashMap<>(); Set configurationsToTakeOver = sourceRepository == null ? ImmutableSet.of() : copyFromRepository(sourceRepository, stringCache, namespaceResolverCache); // If not loading from a jar file, try to load from a cache file first. A separate cache file is not used // when loading from framework_res.jar since it already contains data in the cache format. Loading from // framework_res.jar or a cache file is significantly faster than reading individual resource files. if (!loader.isLoadingFromZipArchive() && cachingData != null) { loadFromPersistentCache(cachingData, languageGroups, languageGroupsLoadedFromSourceRepositoryOrCache, stringCache, namespaceResolverCache); } myLanguageGroups.addAll(languageGroupsLoadedFromSourceRepositoryOrCache); if (languageGroups == null || !languageGroupsLoadedFromSourceRepositoryOrCache.containsAll(languageGroups)) { loader.loadRepositoryContents(this); } myLoadedFromCache = myNumberOfLanguageGroupsLoadedFromCache == myLanguageGroups.size(); populatePublicResourcesMap(); freezeResources(); takeOverConfigurations(configurationsToTakeOver); if (!loader.isLoadingFromZipArchive() && cachingData != null) { Executor executor = cachingData.getCacheCreationExecutor(); if (executor != null && !languageGroupsLoadedFromSourceRepositoryOrCache.containsAll(myLanguageGroups)) { executor.execute(() -> createPersistentCache(cachingData, languageGroupsLoadedFromSourceRepositoryOrCache)); } } } @Override @Nullable public String getPackageName() { return ANDROID_NAMESPACE.getPackageName(); } @Override @NotNull public Set getResourceTypes(@NotNull ResourceNamespace namespace) { return namespace == ANDROID_NAMESPACE ? Sets.immutableEnumSet(myResources.keySet()) : ImmutableSet.of(); } /** * Copies resources from another FrameworkResourceRepository. * * @param sourceRepository the repository to copy resources from * @param stringCache the string cache to populate with the names of copied resources * @param namespaceResolverCache the namespace resolver cache to populate with namespace resolvers referenced by the copied resources * @return the {@link RepositoryConfiguration} objects referenced by the copied resources */ @NotNull private Set copyFromRepository(@NotNull FrameworkResourceRepository sourceRepository, @NotNull Map stringCache, @NotNull Map namespaceResolverCache) { Collection> resourceMaps = sourceRepository.myResources.values(); // Copy resources from the source repository, get AarConfigurations that need to be taken over by this repository, // and pre-populate string and namespace resolver caches. Set sourceConfigurations = Sets.newIdentityHashSet(); for (ListMultimap resourceMap : resourceMaps) { for (ResourceItem item : resourceMap.values()) { addResourceItem(item); sourceConfigurations.add(((BasicResourceItemBase)item).getRepositoryConfiguration()); if (item instanceof BasicValueResourceItemBase) { ResourceNamespace.Resolver resolver = ((BasicValueResourceItemBase)item).getNamespaceResolver(); NamespaceResolver namespaceResolver = resolver == ResourceNamespace.Resolver.EMPTY_RESOLVER ? NamespaceResolver.EMPTY : (NamespaceResolver)resolver; namespaceResolverCache.put(namespaceResolver, namespaceResolver); } String name = item.getName(); stringCache.put(name, name); } } myNumberOfLanguageGroupsLoadedFromCache += sourceRepository.myNumberOfLanguageGroupsLoadedFromCache; return sourceConfigurations; } private void loadFromPersistentCache(@NotNull CachingData cachingData, @Nullable Set languagesToLoad, @NotNull Set loadedLanguages, @NotNull Map stringCache, @Nullable Map namespaceResolverCache) { CacheFileNameGenerator fileNameGenerator = new CacheFileNameGenerator((cachingData)); Set languages = languagesToLoad == null ? fileNameGenerator.getAllCacheFileLanguages() : languagesToLoad; for (String language : languages) { if (!loadedLanguages.contains(language)) { Path cacheFile = fileNameGenerator.getCacheFile(language); try (Base128InputStream stream = new Base128InputStream(cacheFile)) { byte[] header = ResourceSerializationUtil.getCacheFileHeader(s -> writeCacheHeaderContent(cachingData, language, s)); if (!stream.validateContents(header)) { // Cache file header doesn't match. if (language.isEmpty()) { break; // Don't try to load language-specific resources if language-neutral ones could not be loaded. } continue; } loadFromStream(stream, stringCache, namespaceResolverCache); loadedLanguages.add(language); myNumberOfLanguageGroupsLoadedFromCache++; } catch (NoSuchFileException e) { // Cache file does not exist. if (language.isEmpty()) { break; // Don't try to load language-specific resources if language-neutral ones could not be loaded. } } catch (ProcessCanceledException e) { cleanupAfterFailedLoadingFromCache(); loadedLanguages.clear(); throw e; } catch (Throwable e) { cleanupAfterFailedLoadingFromCache(); loadedLanguages.clear(); LOG.warn("Failed to load from cache file " + cacheFile.toString(), e); break; } } } } @Override protected void cleanupAfterFailedLoadingFromCache() { super.cleanupAfterFailedLoadingFromCache(); myNumberOfLanguageGroupsLoadedFromCache = 0; } private void createPersistentCache(@NotNull CachingData cachingData, @NotNull Set languagesToSkip) { CacheFileNameGenerator fileNameGenerator = new CacheFileNameGenerator(cachingData); for (String language : myLanguageGroups) { if (!languagesToSkip.contains(language)) { Path cacheFile = fileNameGenerator.getCacheFile(language); byte[] header = ResourceSerializationUtil.getCacheFileHeader(stream -> writeCacheHeaderContent(cachingData, language, stream)); ResourceSerializationUtil.createPersistentCache( cacheFile, header, stream -> writeToStream(stream, config -> language.equals(getLanguageGroup(config)))); } } } private void writeCacheHeaderContent(@NotNull CachingData cachingData, @NotNull String language, @NotNull Base128OutputStream stream) throws IOException { writeCacheHeaderContent(cachingData, stream); stream.writeString(language); } /** * Returns the name of the resource table file containing resources for the given language. * * @param language the two-letter language abbreviation, or an empty string for language-neutral resources * @return the file name */ static String getResourceTableNameForLanguage(@NotNull String language) { return language.isEmpty() ? "resources.bin" : RESOURCES_TABLE_PREFIX + language + RESOURCE_TABLE_SUFFIX; } @NotNull static String getLanguageGroup(@NotNull FolderConfiguration config) { LocaleQualifier locale = config.getLocaleQualifier(); return locale == null ? "" : getLanguageGroup(StringUtil.notNullize(locale.getLanguage())); } /** * Maps some languages to others effectively grouping languages together. For example, Romansh language * that has very few framework resources is grouped together with Italian. * * @param language the original language * @return the language representing the corresponding group of languages */ @NotNull private static String getLanguageGroup(@NotNull String language) { return LANGUAGE_TO_GROUP.getOrDefault(language, language); } @NotNull private static Set getLanguageGroups(@NotNull Set languages) { Set result = new TreeSet<>(); result.add(""); for (String language : languages) { result.add(getLanguageGroup(language)); } return result; } @NotNull Set getLanguageGroups() { Set languages = new TreeSet<>(); for (ListMultimap resourceMap : myResources.values()) { for (ResourceItem item : resourceMap.values()) { FolderConfiguration config = item.getConfiguration(); languages.add(getLanguageGroup(config)); } } return languages; } private int getNumberOfLanguageGroupsLoadedFromOrigin() { return myLanguageGroups.size() - myNumberOfLanguageGroupsLoadedFromCache; } @TestOnly int getNumberOfLanguageGroupsLoadedFromCache() { return myNumberOfLanguageGroupsLoadedFromCache; } @NotNull private String updateResourcePath(@NotNull String relativeResourcePath) { if (myUseCompiled9Patches && relativeResourcePath.endsWith(DOT_9PNG)) { return StringUtil.replaceSubstring(relativeResourcePath, TextRange.create(relativeResourcePath.length() - DOT_9PNG.length(), relativeResourcePath.length()), COMPILED_9PNG_EXTENSION); } return relativeResourcePath; } @Override @NotNull public String getResourceUrl(@NotNull String relativeResourcePath) { return super.getResourceUrl(updateResourcePath(relativeResourcePath)); } @Override @NotNull public PathString getSourceFile(@NotNull String relativeResourcePath, boolean forFileResource) { return super.getSourceFile(updateResourcePath(relativeResourcePath), forFileResource); } private static class Loader extends RepositoryLoader { @NotNull private final List myPublicFileNames = ImmutableList.of("public.xml", "public-final.xml", "public-staging.xml"); @NotNull private final Set myLoadedLanguageGroups; @Nullable private Set myLanguageGroups; Loader(@NotNull Path resourceDirectoryOrFile, @Nullable Set languageGroups) { super(resourceDirectoryOrFile, null, ANDROID_NAMESPACE); myLanguageGroups = languageGroups; myLoadedLanguageGroups = new TreeSet<>(); } Loader(@NotNull FrameworkResourceRepository sourceRepository, @Nullable Set languageGroups) { super(sourceRepository.myResourceDirectoryOrFile, null, ANDROID_NAMESPACE); myLanguageGroups = languageGroups; myLoadedLanguageGroups = new TreeSet<>(sourceRepository.myLanguageGroups); } public List getPublicXmlFileNames() { return myPublicFileNames; } @Override protected void loadFromZip(@NotNull FrameworkResourceRepository repository) { try (ZipFile zipFile = new ZipFile(myResourceDirectoryOrFile.toFile())) { if (myLanguageGroups == null) { myLanguageGroups = readLanguageGroups(zipFile); } Map stringCache = Maps.newHashMapWithExpectedSize(10000); Map namespaceResolverCache = new HashMap<>(); for (String language : myLanguageGroups) { if (!myLoadedLanguageGroups.contains(language)) { String entryName = getResourceTableNameForLanguage(language); ZipEntry zipEntry = zipFile.getEntry(entryName); if (zipEntry == null) { if (language.isEmpty()) { throw new IOException("\"" + entryName + "\" not found in " + myResourceDirectoryOrFile.toString()); } else { continue; // Requested language may not be represented in the Android framework resources. } } try (Base128InputStream stream = new Base128InputStream(zipFile.getInputStream(zipEntry))) { repository.loadFromStream(stream, stringCache, namespaceResolverCache); } } } repository.populatePublicResourcesMap(); repository.freezeResources(); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { LOG.error("Failed to load resources from " + myResourceDirectoryOrFile.toString(), e); } } @NotNull private static Set readLanguageGroups(@NotNull ZipFile zipFile) { ImmutableSortedSet.Builder result = ImmutableSortedSet.naturalOrder(); result.add(""); zipFile.stream().forEach(entry -> { String name = entry.getName(); if (name.startsWith(RESOURCES_TABLE_PREFIX) && name.endsWith(RESOURCE_TABLE_SUFFIX) && name.length() == RESOURCES_TABLE_PREFIX.length() + RESOURCE_TABLE_SUFFIX.length() + 2 && Character.isLetter(name.charAt(RESOURCES_TABLE_PREFIX.length())) && Character.isLetter(name.charAt(RESOURCES_TABLE_PREFIX.length() + 1))) { result.add(name.substring(RESOURCES_TABLE_PREFIX.length(), RESOURCES_TABLE_PREFIX.length() + 2)); } }); return result.build(); } @Override public void loadRepositoryContents(@NotNull FrameworkResourceRepository repository) { super.loadRepositoryContents(repository); Set languageGroups = myLanguageGroups == null ? repository.getLanguageGroups() : myLanguageGroups; repository.myLanguageGroups.addAll(languageGroups); } @Override public boolean isIgnored(@NotNull Path fileOrDirectory, @NotNull BasicFileAttributes attrs) { if (fileOrDirectory.equals(myResourceDirectoryOrFile)) { return false; } if (super.isIgnored(fileOrDirectory, attrs)) { return true; } String fileName = fileOrDirectory.getFileName().toString(); if (attrs.isDirectory()) { if (fileName.startsWith("values-mcc") || fileName.startsWith(FD_RES_RAW) && (fileName.length() == FD_RES_RAW.length() || fileName.charAt(FD_RES_RAW.length()) == '-')) { return true; // Mobile country codes and raw resources are not used by LayoutLib. } // Skip folders that don't belong to languages in myLanguageGroups or languages that were loaded earlier. if (myLanguageGroups != null || !myLoadedLanguageGroups.isEmpty()) { FolderConfiguration config = FolderConfiguration.getConfigForFolder(fileName); if (config == null) { return true; } String language = getLanguageGroup(config); if ((myLanguageGroups != null && !myLanguageGroups.contains(language)) || myLoadedLanguageGroups.contains(language)) { return true; } myFolderConfigCache.put(config.getQualifierString(), config); } } else if ((myPublicFileNames.contains(fileName) || fileName.equals("symbols.xml")) && "values".equals(new PathString(fileOrDirectory).getParentFileName())) { return true; // Skip files that don't contain resources. } else if (fileName.endsWith(COMPILED_9PNG_EXTENSION)) { return true; } return false; } @Override protected final void addResourceItem(@NotNull BasicResourceItem item, @NotNull FrameworkResourceRepository repository) { repository.addResourceItem(item); } @Override @NotNull protected String getKeyForVisibilityLookup(@NotNull String resourceName) { // This class obtains names of public resources from public.xml where all resource names are preserved // in their original form. This is different from the superclass that obtains the names from public.txt // where the names are transformed by replacing dots, colons and dashes with underscores. return resourceName; } } /** * Redirects the {@link RepositoryConfiguration} inherited from another repository to point to this one, so that * the other repository can be garbage collected. This has to be done after this repository is fully loaded. * * @param sourceConfigurations the configurations to reparent */ private void takeOverConfigurations(@NotNull Set sourceConfigurations) { for (RepositoryConfiguration configuration : sourceConfigurations) { configuration.transferOwnershipTo(this); } } private static class CacheFileNameGenerator { private final Path myLanguageNeutralFile; private final String myPrefix; private final String mySuffix; CacheFileNameGenerator(@NotNull CachingData cachingData) { myLanguageNeutralFile = cachingData.getCacheFile(); String fileName = myLanguageNeutralFile.getFileName().toString(); int dotPos = fileName.lastIndexOf('.'); myPrefix = dotPos >= 0 ? fileName.substring(0, dotPos) : fileName; mySuffix = dotPos >= 0 ? fileName.substring(dotPos) : ""; } @NotNull Path getCacheFile(@NotNull String language) { return language.isEmpty() ? myLanguageNeutralFile : myLanguageNeutralFile.resolveSibling(myPrefix + '_' + language + mySuffix); } /** * Determines language from a cache file name. * * @param cacheFileName the name of a cache file * @return the language of resources contained in the cache file, or null if {@code cacheFileName} * doesn't match the pattern of cache file names. */ @Nullable String getLanguage(@NotNull String cacheFileName) { if (!cacheFileName.startsWith(myPrefix) || !cacheFileName.endsWith(mySuffix)) { return null; } int baseLength = myPrefix.length() + mySuffix.length(); if (cacheFileName.length() == baseLength) { return ""; } if (cacheFileName.length() != baseLength + 3 || cacheFileName.charAt(myPrefix.length()) != '_') { return null; } String language = cacheFileName.substring(myPrefix.length() + 1, myPrefix.length() + 3); if (!isLowerCaseLatinLetter(language.charAt(0)) || !isLowerCaseLatinLetter(language.charAt(1))) { return null; } return language; } @NotNull public Set getAllCacheFileLanguages() { Set result = new TreeSet<>(); try (Stream stream = CancellableFileIo.list(myLanguageNeutralFile.getParent())) { stream.forEach(file -> { String language = getLanguage(file.getFileName().toString()); if (language != null) { result.add(language); } }); } catch (IOException ignore) { } return result; } private static boolean isLowerCaseLatinLetter(char c) { return 'a' <= c && c <= 'z'; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy