| /* |
| * Copyright (C) 2015 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.ide.common.repository; |
| |
| import static com.android.SdkConstants.FN_RESOURCE_TEXT; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.builder.model.AndroidArtifact; |
| import com.android.builder.model.AndroidLibrary; |
| import com.android.builder.model.AndroidProject; |
| import com.android.builder.model.Variant; |
| import com.android.ide.common.resources.ResourceUrl; |
| import com.android.resources.ResourceType; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Multimap; |
| import com.google.common.io.Files; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Class which provides information about whether Android resources for a given library are |
| * public or private. |
| */ |
| public abstract class ResourceVisibilityLookup { |
| /** |
| * Returns true if the given resource is private |
| * |
| * @param type the type of the resource |
| * @param name the resource field name of the resource (in other words, for |
| * style Theme:Variant.Cls the name would be Theme_Variant_Cls; you can use |
| * {@link LintUtils#g} |
| * @return true if the given resource is private |
| */ |
| public abstract boolean isPrivate( |
| @NonNull ResourceType type, |
| @NonNull String name); |
| |
| /** |
| * Returns true if the given resource is private in the library |
| * |
| * @param url the resource URL |
| * @return true if the given resource is private |
| */ |
| public boolean isPrivate(@NonNull ResourceUrl url) { |
| assert !url.framework; // Framework resources are not part of the library |
| return isPrivate(url.type, url.name); |
| } |
| |
| /** |
| * For a private resource, return the {@link AndroidLibrary} that the resource was defined as |
| * private in |
| * |
| * @param type the type of the resource |
| * @param name the name of the resource |
| * @return the library which defines the resource as private |
| */ |
| @Nullable |
| public abstract AndroidLibrary getPrivateIn(@NonNull ResourceType type, @NonNull String name); |
| |
| /** Returns true if this repository does not declare any resources to be private */ |
| public abstract boolean isEmpty(); |
| |
| /** |
| * Creates a {@link ResourceVisibilityLookup} for a given library. |
| * <p> |
| * NOTE: The {@link Provider} class can be used to share/cache {@link ResourceVisibilityLookup} |
| * instances, e.g. when you have library1 and library2 each referencing libraryBase, the {@link |
| * Provider} will ensure that a the libraryBase data is shared. |
| * |
| * @param library the library |
| * @return a corresponding {@link ResourceVisibilityLookup} |
| */ |
| @NonNull |
| public static ResourceVisibilityLookup create(@NonNull AndroidLibrary library) { |
| return new LibraryResourceVisibility(library); |
| } |
| |
| /** |
| * Creates a {@link ResourceVisibilityLookup} for the set of libraries. |
| * <p> |
| * NOTE: The {@link Provider} class can be used to share/cache {@link ResourceVisibilityLookup} |
| * instances, e.g. when you have library1 and library2 each referencing libraryBase, the {@link |
| * Provider} will ensure that a the libraryBase data is shared. |
| * |
| * @param libraries the list of libraries |
| * @param provider an optional manager instance for caching of individual libraries, if any |
| * @return a corresponding {@link ResourceVisibilityLookup} |
| */ |
| @NonNull |
| public static ResourceVisibilityLookup create(@NonNull List<AndroidLibrary> libraries, |
| @Nullable Provider provider) { |
| List<ResourceVisibilityLookup> list = Lists.newArrayListWithExpectedSize(libraries.size()); |
| for (AndroidLibrary library : libraries) { |
| ResourceVisibilityLookup v = provider != null ? provider.get(library) : create(library); |
| if (!v.isEmpty()) { |
| list.add(v); |
| } |
| } |
| return new MultipleLibraryResourceVisibility(list); |
| } |
| |
| public static final ResourceVisibilityLookup NONE = new ResourceVisibilityLookup() { |
| @Override |
| public boolean isPrivate(@NonNull ResourceType type, @NonNull String name) { |
| return false; |
| } |
| |
| @Nullable |
| @Override |
| public AndroidLibrary getPrivateIn(@NonNull ResourceType type, @NonNull String name) { |
| return null; |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return true; |
| } |
| }; |
| |
| /** Searches multiple libraries */ |
| private static class MultipleLibraryResourceVisibility extends ResourceVisibilityLookup { |
| |
| private final List<ResourceVisibilityLookup> mRepositories; |
| |
| public MultipleLibraryResourceVisibility(List<ResourceVisibilityLookup> repositories) { |
| mRepositories = repositories; |
| } |
| |
| // It's anticipated that these methods will be called a lot (e.g. in inner loops |
| // iterating over all resources matching code completion etc) so since we know |
| // that our list has random access, avoid creating iterators here |
| @SuppressWarnings("ForLoopReplaceableByForEach") |
| @Override |
| public boolean isPrivate(@NonNull ResourceType type, @NonNull String name) { |
| for (int i = 0, n = mRepositories.size(); i < n; i++) { |
| if (mRepositories.get(i).isPrivate(type, name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @SuppressWarnings("ForLoopReplaceableByForEach") |
| @Override |
| public boolean isEmpty() { |
| for (int i = 0, n = mRepositories.size(); i < n; i++) { |
| if (!mRepositories.get(i).isEmpty()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @SuppressWarnings("ForLoopReplaceableByForEach") |
| @Nullable |
| @Override |
| public AndroidLibrary getPrivateIn(@NonNull ResourceType type, @NonNull String name) { |
| for (int i = 0, n = mRepositories.size(); i < n; i++) { |
| ResourceVisibilityLookup r = mRepositories.get(i); |
| if (r.isPrivate(type, name)) { |
| return r.getPrivateIn(type, name); |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Provider which keeps a set of {@link ResourceVisibilityLookup} instances around for |
| * repeated queries, including from different libraries that may share dependencies |
| */ |
| public static class Provider { |
| /** |
| * We store lookup instances for multiple separate types of keys here: |
| * {@link AndroidLibrary}, {@link AndroidArtifact}, and {@link Variant} |
| */ |
| private Map<Object, ResourceVisibilityLookup> mInstances = Maps.newHashMap(); |
| |
| /** |
| * Looks up a (possibly cached) {@link ResourceVisibilityLookup} for the given {@link |
| * AndroidLibrary} |
| * |
| * @param library the library |
| * @return the corresponding {@link ResourceVisibilityLookup} |
| */ |
| @NonNull |
| public ResourceVisibilityLookup get(@NonNull AndroidLibrary library) { |
| ResourceVisibilityLookup visibility = mInstances.get(library); |
| if (visibility == null) { |
| visibility = new LibraryResourceVisibility(library); |
| if (visibility.isEmpty()) { |
| visibility = NONE; |
| } |
| List<? extends AndroidLibrary> dependsOn = library.getLibraryDependencies(); |
| if (!dependsOn.isEmpty()) { |
| List<ResourceVisibilityLookup> list = |
| Lists.newArrayListWithExpectedSize(dependsOn.size() + 1); |
| list.add(visibility); |
| for (AndroidLibrary d : dependsOn) { |
| ResourceVisibilityLookup v = get(d); |
| if (!v.isEmpty()) { |
| list.add(v); |
| } |
| } |
| if (list.size() > 1) { |
| visibility = new MultipleLibraryResourceVisibility(list); |
| } |
| } |
| mInstances.put(library, visibility); |
| } |
| return visibility; |
| } |
| |
| /** |
| * Looks up a (possibly cached) {@link ResourceVisibilityLookup} for the given {@link |
| * AndroidArtifact} |
| * |
| * @param artifact the artifact |
| * @return the corresponding {@link ResourceVisibilityLookup} |
| */ |
| @NonNull |
| public ResourceVisibilityLookup get(@NonNull AndroidArtifact artifact) { |
| ResourceVisibilityLookup visibility = mInstances.get(artifact); |
| if (visibility == null) { |
| Collection<AndroidLibrary> dependsOn = artifact.getDependencies().getLibraries(); |
| List<ResourceVisibilityLookup> list = |
| Lists.newArrayListWithExpectedSize(dependsOn.size() + 1); |
| for (AndroidLibrary d : dependsOn) { |
| ResourceVisibilityLookup v = get(d); |
| if (!v.isEmpty()) { |
| list.add(v); |
| } |
| } |
| int size = list.size(); |
| visibility = size == 0 ? NONE : size == 1 ? list.get(0) : new MultipleLibraryResourceVisibility(list); |
| mInstances.put(artifact, visibility); |
| } |
| return visibility; |
| } |
| |
| /** |
| * Returns true if the given Gradle model is compatible with public resources. |
| * (Older models than 1.3 will throw exceptions if we attempt to for example |
| * query the public resource file location. |
| * |
| * @param project the project to check |
| * @return true if the model is recent enough to support resource visibility queries |
| */ |
| public static boolean isVisibilityAwareModel(@NonNull AndroidProject project) { |
| String modelVersion = project.getModelVersion(); |
| // getApiVersion doesn't work prior to 1.2, and API level must be at least 3 |
| return !(modelVersion.startsWith("1.0") || modelVersion.startsWith("1.1")) |
| && project.getApiVersion() >= 3; |
| } |
| |
| /** |
| * Looks up a (possibly cached) {@link ResourceVisibilityLookup} for the given {@link |
| * AndroidArtifact} |
| * |
| * @param project the project |
| * @return the corresponding {@link ResourceVisibilityLookup} |
| */ |
| @NonNull |
| public ResourceVisibilityLookup get( |
| @NonNull AndroidProject project, |
| @NonNull Variant variant) { |
| ResourceVisibilityLookup visibility = mInstances.get(variant); |
| if (visibility == null) { |
| if (isVisibilityAwareModel(project)) { |
| AndroidArtifact artifact = variant.getMainArtifact(); |
| visibility = get(artifact); |
| } else { |
| visibility = NONE; |
| } |
| mInstances.put(variant, visibility); |
| } |
| return visibility; |
| } |
| } |
| |
| /** Visibility data for a single library */ |
| private static class LibraryResourceVisibility extends ResourceVisibilityLookup { |
| private final AndroidLibrary mLibrary; |
| private final Multimap<String, ResourceType> mAll; |
| private final Multimap<String, ResourceType> mPublic; |
| |
| private LibraryResourceVisibility(@NonNull AndroidLibrary library) { |
| mLibrary = library; |
| |
| mPublic = computeVisibilityMap(); |
| //noinspection VariableNotUsedInsideIf |
| if (mPublic != null) { |
| mAll = computeAllMap(); |
| } else { |
| mAll = null; |
| } |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return mPublic == null; |
| } |
| |
| @Nullable |
| @Override |
| public AndroidLibrary getPrivateIn(@NonNull ResourceType type, @NonNull String name) { |
| if (isPrivate(type, name)) { |
| return mLibrary; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns a map from name to applicable resource types where the presence of the type+name |
| * combination means that the corresponding resource is explicitly public. |
| * |
| * If the result is null, there is no {@code public.txt} definition for this library, so all |
| * resources should be taken to be public. |
| * |
| * @return a map from name to resource type for public resources in this library |
| */ |
| @Nullable |
| private Multimap<String, ResourceType> computeVisibilityMap() { |
| File publicResources = mLibrary.getPublicResources(); |
| if (!publicResources.exists()) { |
| return null; |
| } |
| |
| try { |
| List<String> lines = Files.readLines(publicResources, Charsets.UTF_8); |
| Multimap<String, ResourceType> result = ArrayListMultimap.create(lines.size(), 2); |
| for (String line : lines) { |
| // These files are written by code in MergedResourceWriter#postWriteAction |
| // Format for each line: <type><space><name>\n |
| // Therefore, we don't expect/allow variations in the format (we don't |
| // worry about extra spaces needing to be trimmed etc) |
| int index = line.indexOf(' '); |
| if (index == -1 || line.isEmpty()) { |
| continue; |
| } |
| |
| String typeString = line.substring(0, index); |
| ResourceType type = ResourceType.getEnum(typeString); |
| if (type == null) { |
| // This could in theory happen if in the future a new ResourceType is |
| // introduced, and a newer version of the Gradle build system writes the |
| // name of this type into the public.txt file, and an older version of |
| // the IDE then attempts to read it. Just skip these symbols. |
| continue; |
| } |
| String name = line.substring(index + 1); |
| result.put(name, type); |
| } |
| return result; |
| } catch (IOException ignore) { |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a map from name to resource types for all resources known to this library. This |
| * is used to make sure that when the {@link #isPrivate(ResourceType, String)} query method |
| * is called, it can tell the difference between a resource implicitly private by not being |
| * declared as public and a resource unknown to this library (e.g. defined by a different |
| * library or the user's own project resources.) |
| * |
| * @return a map from name to resource type for all resources in this library |
| */ |
| @Nullable |
| private Multimap<String, ResourceType> computeAllMap() { |
| // getSymbolFile() is not defined in AndroidLibrary, only in the subclass LibraryBundle |
| File symbolFile = new File(mLibrary.getPublicResources().getParentFile(), |
| FN_RESOURCE_TEXT); |
| if (!symbolFile.exists()) { |
| return null; |
| } |
| |
| try { |
| List<String> lines = Files.readLines(symbolFile, Charsets.UTF_8); |
| Multimap<String, ResourceType> result = ArrayListMultimap.create(lines.size(), 2); |
| |
| ResourceType previousType = null; |
| String previousTypeString = ""; |
| int lineIndex = 1; |
| final int count = lines.size(); |
| for (; lineIndex <= count; lineIndex++) { |
| String line = lines.get(lineIndex - 1); |
| |
| if (line.startsWith("int ")) { // not int[] definitions for styleables |
| // format is "int <type> <class> <name> <value>" |
| int typeStart = 4; |
| int typeEnd = line.indexOf(' ', typeStart); |
| |
| // Items are sorted by type, so we can avoid looping over types in |
| // ResourceType.getEnum() for each line by sharing type in each section |
| String typeString = line.substring(typeStart, typeEnd); |
| ResourceType type; |
| if (typeString.equals(previousTypeString)) { |
| type = previousType; |
| } else { |
| type = ResourceType.getEnum(typeString); |
| previousTypeString = typeString; |
| previousType = type; |
| } |
| if (type == null) { // some newly introduced type |
| continue; |
| } |
| |
| int nameStart = typeEnd + 1; |
| int nameEnd = line.indexOf(' ', nameStart); |
| String name = line.substring(nameStart, nameEnd); |
| result.put(name, type); |
| } |
| } |
| return result; |
| } catch (IOException ignore) { |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if the given resource is private in the library |
| * |
| * @param type the type of the resource |
| * @param name the name of the resource |
| * @return true if the given resource is private |
| */ |
| @Override |
| public boolean isPrivate(@NonNull ResourceType type, @NonNull String name) { |
| //noinspection SimplifiableIfStatement |
| if (mPublic == null) { |
| // No public definitions: Everything assumed to be public |
| return false; |
| } |
| |
| //noinspection SimplifiableIfStatement |
| if (!mAll.containsEntry(name, type)) { |
| // Don't respond to resource URLs that are not part of this project |
| // since we won't have private information on them |
| return false; |
| } |
| return !mPublic.containsEntry(name, type); |
| } |
| } |
| } |