com.android.tools.idea.res.AppResourceRepository.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.res.AppResourceRepository.java

Source

/*
 * Copyright (C) 2016 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.idea.res;

import com.android.annotations.VisibleForTesting;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.Variant;
import com.android.ide.common.rendering.api.AttrResourceValue;
import com.android.ide.common.repository.GradleVersion;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.ide.common.resources.IntArrayWrapper;
import com.android.resources.ResourceType;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.util.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TObjectIntHashMap;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.uipreview.ModuleClassLoader;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.*;

import static com.android.SdkConstants.DOT_AAR;
import static com.android.SdkConstants.FD_RES;
import static com.android.tools.idea.LogAnonymizerUtil.anonymizeClassName;
import static org.jetbrains.android.facet.ResourceFolderManager.addAarsFromModuleLibraries;

/**
 * Resource repository which merges in resources from the libraries and modules which are
 * transitive dependencies of the given AndroidFacet / module.
 */
public class AppResourceRepository extends MultiResourceRepository {
    private static final Logger LOG = Logger.getInstance(AppResourceRepository.class);

    private final AndroidFacet myFacet;
    private List<FileResourceRepository> myLibraries;
    private long myIdsModificationCount;

    /**
     * List of libraries that contain an R.txt file.
     *
     * The order of these libraries may not match the order of {@link #myLibraries}. It's intended to be used
     * only to get the R.txt files for declare styleables.
     */
    private final LinkedList<FileResourceRepository> myAarLibraries = Lists.newLinkedList();
    private Set<String> myIds;

    protected AppResourceRepository(@NotNull AndroidFacet facet,
            @NotNull List<? extends LocalResourceRepository> delegates,
            @NotNull List<FileResourceRepository> libraries) {
        super(facet.getModule().getName() + " with modules and libraries", delegates);
        myFacet = facet;
        myLibraries = libraries;
        for (FileResourceRepository library : libraries) {
            if (library.getResourceTextFile() != null) {
                myAarLibraries.add(library);
            }
        }
    }

    /**
     * Returns the Android merge resource repository for the resources in this module, any other modules in this project,
     * and any libraries this project depends on.
     *
     * @param module            the module to look up resources for
     * @param createIfNecessary if true, create the app resources if necessary, otherwise only return if already computed
     * @return the resource repository
     */
    @Nullable
    public static AppResourceRepository getAppResources(@NotNull Module module, boolean createIfNecessary) {
        AndroidFacet facet = AndroidFacet.getInstance(module);
        if (facet != null) {
            return facet.getAppResources(createIfNecessary);
        }

        return null;
    }

    /**
     * Returns the Android merge resource repository for the resources in this module, any other modules in this project,
     * and any libraries this project depends on.
     *
     * @param facet             the module facet to look up resources for
     * @param createIfNecessary if true, create the app resources if necessary, otherwise only return if already computed
     * @return the resource repository
     */
    @Contract("!null, true -> !null")
    @Nullable
    public static AppResourceRepository getAppResources(@NotNull AndroidFacet facet, boolean createIfNecessary) {
        return facet.getAppResources(createIfNecessary);
    }

    @NotNull
    public static AppResourceRepository create(@NotNull AndroidFacet facet) {
        List<FileResourceRepository> libraries = computeLibraries(facet);
        List<LocalResourceRepository> delegates = computeRepositories(facet, libraries);
        AppResourceRepository repository = new AppResourceRepository(facet, delegates, libraries);

        ProjectResourceRepositoryRootListener.ensureSubscribed(facet.getModule().getProject());

        return repository;
    }

    private static List<LocalResourceRepository> computeRepositories(@NotNull final AndroidFacet facet,
            List<FileResourceRepository> libraries) {
        List<LocalResourceRepository> repositories = Lists.newArrayListWithExpectedSize(10);
        LocalResourceRepository resources = ProjectResourceRepository.getProjectResources(facet, true);
        repositories.addAll(libraries);
        repositories.add(resources);
        return repositories;
    }

    private static List<FileResourceRepository> computeLibraries(@NotNull final AndroidFacet facet) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("computeLibraries");
        }

        List<AndroidFacet> dependentFacets = AndroidUtils.getAllAndroidDependencies(facet.getModule(), true);
        Map<File, String> aarDirs = findAarLibraries(facet, dependentFacets);

        if (aarDirs.isEmpty()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("  No AARs");
            }

            return Collections.emptyList();
        }

        List<File> dirs = Lists.newArrayList(aarDirs.keySet());

        // Sort alphabetically to ensure that we keep a consistent order of these libraries;
        // otherwise when we jump from libraries initialized from IntelliJ library binary paths
        // to gradle project state, the order difference will cause the merged project resource
        // maps to have to be recomputed
        Collections.sort(dirs);

        if (LOG.isDebugEnabled()) {
            for (File root : dirs) {
                LOG.debug("  Dependency: " + anonymizeClassName(aarDirs.get(root)));
            }
        }

        List<FileResourceRepository> resources = Lists.newArrayListWithExpectedSize(aarDirs.size());
        for (File root : dirs) {
            resources.add(FileResourceRepository.get(root, aarDirs.get(root)));
        }
        return resources;
    }

    @NotNull
    private static Map<File, String> findAarLibraries(@NotNull AndroidFacet facet,
            @NotNull List<AndroidFacet> dependentFacets) {
        // Use the gradle model if available, but if not, fall back to using plain IntelliJ library dependencies
        // which have been persisted since the most recent sync
        AndroidModuleModel androidModuleModel = AndroidModuleModel.get(facet);
        if (androidModuleModel != null) {
            List<AndroidLibrary> libraries = Lists.newArrayList();
            addGradleLibraries(libraries, androidModuleModel);
            for (AndroidFacet dependentFacet : dependentFacets) {
                AndroidModuleModel dependentGradleModel = AndroidModuleModel.get(dependentFacet);
                if (dependentGradleModel != null) {
                    addGradleLibraries(libraries, dependentGradleModel);
                }
            }
            return findAarLibrariesFromGradle(androidModuleModel.getModelVersion(), dependentFacets, libraries);
        }
        return findAarLibrariesFromIntelliJ(facet, dependentFacets);
    }

    @NotNull
    public static Collection<AndroidLibrary> findAarLibraries(@NotNull AndroidFacet facet) {
        List<AndroidLibrary> libraries = Lists.newArrayList();
        if (facet.requiresAndroidModel()) {
            AndroidModuleModel androidModel = AndroidModuleModel.get(facet);
            if (androidModel != null) {
                List<AndroidFacet> dependentFacets = AndroidUtils.getAllAndroidDependencies(facet.getModule(),
                        true);
                addGradleLibraries(libraries, androidModel);
                for (AndroidFacet dependentFacet : dependentFacets) {
                    AndroidModuleModel dependentGradleModel = AndroidModuleModel.get(dependentFacet);
                    if (dependentGradleModel != null) {
                        addGradleLibraries(libraries, dependentGradleModel);
                    }
                }
            }
        }
        return libraries;
    }

    /**
     * Reads IntelliJ library definitions ({@link com.intellij.openapi.roots.LibraryOrSdkOrderEntry}) and if possible, finds a corresponding
     * {@code .aar} resource library to include. This works before the Gradle project has been initialized.
     */
    private static Map<File, String> findAarLibrariesFromIntelliJ(AndroidFacet facet,
            List<AndroidFacet> dependentFacets) {
        // Find .aar libraries from old IntelliJ library definitions
        Map<File, String> dirs = new HashMap<>();
        addAarsFromModuleLibraries(facet, dirs);
        for (AndroidFacet f : dependentFacets) {
            addAarsFromModuleLibraries(f, dirs);
        }
        return dirs;
    }

    /**
     * Looks up the library dependencies from the Gradle tools model and returns the corresponding {@code .aar}
     * resource directories.
     */
    @NotNull
    private static Map<File, String> findAarLibrariesFromGradle(@NotNull GradleVersion modelVersion,
            List<AndroidFacet> dependentFacets, List<AndroidLibrary> libraries) {
        // Pull out the unique directories, in case multiple modules point to the same .aar folder
        Map<File, String> files = new HashMap<>(libraries.size());

        Set<String> moduleNames = Sets.newHashSet();
        for (AndroidFacet f : dependentFacets) {
            moduleNames.add(f.getModule().getName());
        }
        try {
            for (AndroidLibrary library : libraries) {
                // We should only add .aar dependencies if they aren't already provided as modules.
                // For now, the way we associate them with each other is via the library name;
                // in the future the model will provide this for us
                String libraryName = null;
                String projectName = library.getProject();
                if (projectName != null && !projectName.isEmpty()) {
                    libraryName = projectName.substring(projectName.lastIndexOf(':') + 1);
                    // Since this library has project!=null, it exists in module form; don't
                    // add it here.
                    moduleNames.add(libraryName);
                    continue;
                } else {
                    File folder = library.getFolder();
                    String name = folder.getName();
                    if (modelVersion.getMajor() > 2
                            || modelVersion.getMajor() == 2 && modelVersion.getMinor() >= 2) {
                        // Library.getName() was added in 2.2
                        libraryName = library.getName();
                    } else if (name.endsWith(DOT_AAR)) {
                        libraryName = name.substring(0, name.length() - DOT_AAR.length());
                    } else if (folder.getPath().contains(AndroidModuleModel.EXPLODED_AAR)) {
                        libraryName = folder.getParentFile().getName();
                    }
                }
                if (libraryName != null && !moduleNames.contains(libraryName)) {
                    File resFolder = library.getResFolder();
                    if (resFolder.exists()) {
                        files.put(resFolder, libraryName);

                        // Don't add it again!
                        moduleNames.add(libraryName);
                    }
                }
            }
        } catch (UnsupportedMethodException e) {
            // This happens when there is an incompatibility between the builder-model interfaces embedded in Android Studio and the
            // cached model.
            // If we got here is because this code got invoked before project sync happened (e.g. when reopening a project with open editors).
            // Project sync now is smart enough to handle this case and will trigger a full sync.
            LOG.warn("Incompatibility found between the IDE's builder-model and the cached Gradle model", e);
        }
        return files;
    }

    // TODO: b/23032391
    private static void addGradleLibraries(List<AndroidLibrary> list, AndroidModuleModel androidModuleModel) {
        Collection<AndroidLibrary> libraries = androidModuleModel.getSelectedMainCompileDependencies()
                .getLibraries();
        Set<File> unique = Sets.newHashSet();
        for (AndroidLibrary library : libraries) {
            addGradleLibrary(list, library, unique);
        }
    }

    private static void addGradleLibrary(List<AndroidLibrary> list, AndroidLibrary library, Set<File> unique) {
        File folder = library.getFolder();
        if (!unique.add(folder)) {
            return;
        }
        list.add(library);
        for (AndroidLibrary dependency : library.getLibraryDependencies()) {
            addGradleLibrary(list, dependency, unique);
        }
    }

    /** Returns the libraries among the app resources, if any */
    @NotNull
    public List<FileResourceRepository> getLibraries() {
        return myLibraries;
    }

    @NotNull
    private Set<String> getAllIds() {
        long currentModCount = getModificationCount();
        if (myIdsModificationCount < currentModCount) {
            myIdsModificationCount = currentModCount;
            if (myIds == null) {
                int size = 0;
                for (FileResourceRepository library : myLibraries) {
                    if (library.getAllDeclaredIds() != null) {
                        size += library.getAllDeclaredIds().size();
                    }
                }
                myIds = Sets.newHashSetWithExpectedSize(size);
            } else {
                myIds.clear();
            }
            for (FileResourceRepository library : myLibraries) {
                if (library.getAllDeclaredIds() != null) {
                    myIds.addAll(library.getAllDeclaredIds());
                }
            }
            // Also add all ids from resource types, just in case it contains things that are not in the libraries.
            myIds.addAll(super.getItemsOfType(ResourceType.ID));
        }
        return myIds;
    }

    @Override
    @NotNull
    public Collection<String> getItemsOfType(@NotNull ResourceType type) {
        synchronized (ITEM_MAP_LOCK) {
            return type == ResourceType.ID ? getAllIds() : super.getItemsOfType(type);
        }
    }

    void updateRoots() {
        List<FileResourceRepository> libraries = computeLibraries(myFacet);
        List<LocalResourceRepository> repositories = computeRepositories(myFacet, libraries);
        updateRoots(repositories, libraries);
    }

    @VisibleForTesting
    void updateRoots(List<LocalResourceRepository> resources, List<FileResourceRepository> libraries) {
        myResourceVisibility = null;
        myResourceVisibilityProvider = null;
        invalidateResourceDirs();

        if (resources.equals(myChildren)) {
            // Nothing changed (including order); nothing to do
            return;
        }

        myResourceVisibility = null;
        myLibraries = libraries;
        myAarLibraries.clear();
        for (FileResourceRepository library : myLibraries) {
            if (library.getResourceTextFile() != null) {
                myAarLibraries.add(library);
            }
        }
        setChildren(resources);

        // Clear the fake R class cache and the ModuleClassLoader cache.
        resetDynamicIds(true);
        ModuleClassLoader.clearCache(myFacet.getModule());
    }

    @VisibleForTesting
    @NotNull
    static AppResourceRepository createForTest(@NotNull AndroidFacet facet,
            @NotNull List<LocalResourceRepository> modules, @NotNull List<FileResourceRepository> libraries) {
        assert modules.containsAll(libraries);
        assert modules.size() == libraries.size() + 1; // should only combine with the module set repository
        return new AppResourceRepository(facet, modules, libraries);
    }

    @Nullable
    public FileResourceRepository findRepositoryFor(@NotNull File aarDirectory) {
        String aarPath = aarDirectory.getPath();
        for (LocalResourceRepository r : myLibraries) {
            if (r instanceof FileResourceRepository) {
                FileResourceRepository repository = (FileResourceRepository) r;
                if (repository.getResourceDirectory().getPath().startsWith(aarPath)) {
                    return repository;
                }
            } else {
                assert false : r.getClass();
            }
        }

        // If we're looking for an AAR archive and didn't find it above, also
        // attempt searching by suffix alone. This is helpful scenarios like
        // http://b.android.com/210062 where we can end up in a scenario where
        // we're rendering in a library module, and Gradle sync has mapped an
        // AAR library to an existing library definition in the main module. In
        // that case we need to find the corresponding resources there.
        int exploded = aarPath.indexOf(AndroidModuleModel.EXPLODED_AAR);
        if (exploded != -1) {
            String suffix = aarPath.substring(exploded) + File.separator + FD_RES;
            for (LocalResourceRepository r : myLibraries) {
                if (r instanceof FileResourceRepository) {
                    FileResourceRepository repository = (FileResourceRepository) r;
                    String path = repository.getResourceDirectory().getPath();
                    if (path.endsWith(suffix)) {
                        return repository;
                    }
                } else {
                    assert false : r.getClass();
                }
            }
        }

        return null;
    }

    private ResourceVisibilityLookup myResourceVisibility;
    private ResourceVisibilityLookup.Provider myResourceVisibilityProvider;

    @Nullable
    public ResourceVisibilityLookup.Provider getResourceVisibilityProvider() {
        if (myResourceVisibilityProvider == null) {
            if (!myFacet.requiresAndroidModel() || myFacet.getAndroidModel() == null) {
                return null;
            }
            myResourceVisibilityProvider = new ResourceVisibilityLookup.Provider();
        }

        return myResourceVisibilityProvider;
    }

    @NotNull
    public ResourceVisibilityLookup getResourceVisibility(@NotNull AndroidFacet facet) {
        // TODO: b/23032391
        AndroidModuleModel androidModel = AndroidModuleModel.get(facet);
        if (androidModel != null) {
            ResourceVisibilityLookup.Provider provider = getResourceVisibilityProvider();
            if (provider != null) {
                AndroidProject androidProject = androidModel.getAndroidProject();
                Variant variant = androidModel.getSelectedVariant();
                return provider.get(androidProject, variant);
            }
        }

        return ResourceVisibilityLookup.NONE;
    }

    /**
     * Returns true if the given resource is private
     *
     * @param type the type of the resource
     * @param name the name of the resource
     * @return true if the given resource is private
     */
    public boolean isPrivate(@NotNull ResourceType type, @NotNull String name) {
        if (myResourceVisibility == null) {
            ResourceVisibilityLookup.Provider provider = getResourceVisibilityProvider();
            if (provider == null) {
                return false;
            }
            // TODO: b/23032391
            AndroidModuleModel androidModel = AndroidModuleModel.get(myFacet);
            if (androidModel == null) {
                // normally doesn't happen since we check in getResourceVisibility,
                // but can be triggered during a sync (b/22523040)
                return false;
            }
            myResourceVisibility = provider.get(androidModel.getAndroidProject(),
                    androidModel.getSelectedVariant());
        }

        return myResourceVisibility.isPrivate(type, name);
    }

    // For LayoutlibCallback

    // Project resource ints are defined as 0x7FXX#### where XX is the resource type (layout, drawable,
    // etc...). Using FF as the type allows for 255 resource types before we get a collision
    // which should be fine.
    private static final int DYNAMIC_ID_SEED_START = 0x7fff0000;

    /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */
    private Map<ResourceType, TObjectIntHashMap<String>> myResourceValueMap;
    /** Map of (id, [name, resType]) for all resources coming from R.java */
    @SuppressWarnings("deprecation") // For Pair
    private TIntObjectHashMap<Pair<ResourceType, String>> myResIdValueToNameMap;
    /** Map of (int[], name) for styleable resources coming from R.java */
    private Map<IntArrayWrapper, String> myStyleableValueToNameMap;

    private final TObjectIntHashMap<TypedResourceName> myName2DynamicIdMap = new TObjectIntHashMap<TypedResourceName>();
    private final TIntObjectHashMap<TypedResourceName> myDynamicId2ResourceMap = new TIntObjectHashMap<TypedResourceName>();
    private int myDynamicSeed = DYNAMIC_ID_SEED_START;
    private final IntArrayWrapper myWrapper = new IntArrayWrapper(null);

    @Nullable
    @SuppressWarnings("deprecation") // For Pair
    public Pair<ResourceType, String> resolveResourceId(int id) {
        Pair<ResourceType, String> result = null;
        if (myResIdValueToNameMap != null) {
            result = myResIdValueToNameMap.get(id);
        }

        if (result == null) {
            final TypedResourceName pair = myDynamicId2ResourceMap.get(id);
            if (pair != null) {
                result = pair.toPair();
            }
        }

        return result;
    }

    @Nullable
    public String resolveStyleable(int[] id) {
        if (myStyleableValueToNameMap != null) {
            myWrapper.set(id);
            // A normal map lookup on int[] would only consider object identity, but the IntArrayWrapper
            // will check all the individual elements for equality. We reuse an instance for all the lookups
            // since we don't need a new one each time.
            return myStyleableValueToNameMap.get(myWrapper);
        }

        return null;
    }

    @NotNull
    public Integer getResourceId(ResourceType type, String name) {
        final TObjectIntHashMap<String> map = myResourceValueMap != null ? myResourceValueMap.get(type) : null;

        if (map == null || !map.containsKey(name)) {
            return getDynamicId(type, name);
        }
        return map.get(name);
    }

    @Nullable
    Integer[] getDeclaredArrayValues(List<AttrResourceValue> attrs, String styleableName) {
        ListIterator<FileResourceRepository> iter = myAarLibraries.listIterator();
        while (iter.hasNext()) {
            FileResourceRepository repo = iter.next();
            File resourceTextFile = repo.getResourceTextFile();
            if (resourceTextFile == null) {
                continue;
            }
            Integer[] in = RDotTxtParser.getDeclareStyleableArray(resourceTextFile, attrs, styleableName);
            if (in != null) {
                // Reorder the list to place this library first. It's likely that there will be more calls to the same library.
                iter.remove();
                myAarLibraries.addFirst(repo);
                return in;
            }
        }
        return null;
    }

    private int getDynamicId(ResourceType type, String name) {
        TypedResourceName key = new TypedResourceName(type, name);
        synchronized (myName2DynamicIdMap) {
            if (myName2DynamicIdMap.containsKey(key)) {
                return myName2DynamicIdMap.get(key);
            }
            final int value = ++myDynamicSeed;
            myName2DynamicIdMap.put(key, value);
            myDynamicId2ResourceMap.put(value, key);
            return value;
        }
    }

    public void setCompiledResources(
            @SuppressWarnings("deprecation") TIntObjectHashMap<Pair<ResourceType, String>> id2res,
            Map<IntArrayWrapper, String> styleableId2name, Map<ResourceType, TObjectIntHashMap<String>> res2id) {
        myResourceValueMap = res2id;
        myResIdValueToNameMap = id2res;
        myStyleableValueToNameMap = styleableId2name;
    }

    public void resetDynamicIds(boolean clearResourceRegistry) {
        // The dynamic ids are referenced by the generated R classes. Ensure that the R classes cache is also cleared
        // if the dynamic ids are reset.
        if (clearResourceRegistry) {
            ResourceClassRegistry.get(myFacet.getModule().getProject()).clearCache(this);
        }
        synchronized (myName2DynamicIdMap) {
            myDynamicSeed = DYNAMIC_ID_SEED_START;
            myName2DynamicIdMap.clear();
            myDynamicId2ResourceMap.clear();
        }
    }

    private static final class TypedResourceName {
        @Nullable
        final ResourceType myType;
        @NotNull
        final String myName;
        @SuppressWarnings("deprecation")
        Pair<ResourceType, String> myPair;

        public TypedResourceName(@Nullable ResourceType type, @NotNull String name) {
            myType = type;
            myName = name;
        }

        @SuppressWarnings("deprecation")
        public Pair<ResourceType, String> toPair() {
            if (myPair == null) {
                myPair = Pair.of(myType, myName);
            }
            return myPair;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            TypedResourceName that = (TypedResourceName) o;

            if (myType != that.myType)
                return false;
            if (!myName.equals(that.myName))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = myType != null ? myType.hashCode() : 0;
            result = 31 * result + (myName.hashCode());
            return result;
        }

        @Override
        public String toString() {
            return String.format("Type=%1$s, value=%2$s", myType, myName);
        }
    }
}