blob: f30e921fed577aa6cbf2ed94c1559d9ce25b2db2 [file] [log] [blame]
/*
* 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);
}
}
}