/* * Copyright (C) 2013 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.rendering; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceFile; import com.android.ide.common.res2.ResourceItem; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.resources.FolderTypeRelationship; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.tools.lint.detector.api.LintUtils; import com.google.common.collect.Lists; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.ModificationTracker; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.List; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; /** * Repository for Android application resources, e.g. those that show up in {@code R}, not {@code android.R} * (which are referred to as framework resources.). Note that this includes resources from Gradle libraries * too, even though you may not think of these as "local" (they do however (a) end up in the application * namespace, and (b) get extracted by Gradle into the project's build folder where they are merged with * the other resources.) * <p> * For a given Android module, you can obtain either the resources for the module itself, or for a module and all * its libraries. Most clients should use the module with all its dependencies included; when a user is * using code completion for example, they expect to be offered not just the drawables in this module, but * all the drawables available in this module which includes the libraries. * </p> * <p> * The module repository is implemented using several layers. Consider a Gradle project where the main module has * two flavors, and depends on a library module. In this case, the {@linkplain LocalResourceRepository} for the * module with dependencies will contain these components: * <ul> * <li> A {@link com.android.tools.idea.rendering.AppResourceRepository} which contains a * {@link FileResourceRepository} wrapping each AAR library dependency, and merges this with * the project resource repository </li> * <li> A {@link ProjectResourceRepository} representing the collection of module repositories</li> * <li> For each module (e.g. the main module and library module}, a {@link ModuleResourceRepository}</li> * <li> For each resource directory in each module, a {@link ResourceFolderRepository}</li> * </ul> * These different repositories are merged together by the {@link MultiResourceRepository} class, * which represents a repository that just combines the resources from each of its children. * All of {@linkplain AppResourceRepository}, {@linkplain ModuleResourceRepository} and * {@linkplain ProjectResourceRepository} are instances of a {@linkplain MultiResourceRepository}. * </p> * <p> * The {@link ResourceFolderRepository} is the lowest level of repository. It is associated with just * a single resource folder. Therefore, it does not have to worry about trying to mask resources between * different flavors; that task is done by the {@link ModuleResourceRepository} which combines * {@linkplain ResourceFolderRepository} instances. Instead, the {@linkplain ResourceFolderRepository} just * needs to compute the resource items for the resource folders, including qualifier variations. * </p> * <p> * The resource repository automatically stays up to date. You can call {@linkplain #getModificationCount()} * to see whether anything has changed since your last data fetch. This is for example how the resource * string folding in the source editors work; they fetch the current values of the resource strings, and * store those along with the current project resource modification count into the folding data structures. * When the editor wants to see if the folding sections are up to date, those are compared with the current * {@linkplain #getModificationCount()} version, and only if they differ is the folding structure updated. * </p> * <p> * Only the {@linkplain ResourceFolderRepository} needs to listen for user edits and file changes. It * uses {@linkplain PsiProjectListener}, a single listener which is shared by all repositories in the * same project, to get notified when something in one of its resource files changes, and it uses the * PSI change event to selectively update the repository data structures, if possible. * </p> * <p> * The {@linkplain ResourceFolderRepository} can also have a pointer to its parent. This is possible * since a resource folder can only be in a single module. The parent reference is used to quickly * invalidate the cache of the parent {@link MultiResourceRepository}. For example, let's say the * project has two flavors. When the PSI change event is used to update the name of a string resource, * the repository will also notify the parent that its {@link ResourceType#ID} map is out of date. * The {@linkplain MultiResourceRepository} will use this to null out its map cache of strings, and * on the next read, it will merge in the string maps from all its {@linkplain ResourceFolderRepository} * children. * </p> * <p> * One common type of "update" is changing the current variant in the IDE. With the above scheme, * this just means reordering the {@linkplain ResourceFolderRepository} instances in the * {@linkplain ModuleResourceRepository}; it does not have to rescan the resources as it did in the * previous implementation. * </p> * <p> * The {@linkplain ProjectResourceRepository} is similar, but it combines {@link ModuleResourceRepository} * instances rather than {@link ResourceFolderRepository} instances. Note also that the way these * resource repositories work is slightly different from the way the resource items are used by * the builder: The builder will bail if it encounters duplicate declarations unless they are in alternative * folders of the same flavor. For the resource repository we never want to bail on merging; the repository * is kept up to date and live as the user is editing, so it is normal for the repository to sometimes * reflect invalid user edits (in the same way a Java editor in an IDE sometimes is showing uncompilable * source code) and it needs to be able to handle this case and offer a state that is as close to possible * as the intended meaning. Error handling is done by another part of the IDE. * </p> * <p> * Finally, note that the resource repository is showing the current state of the resources for the * currently selected variant. Note however that the above approach also lets us query resources for * example for <b>all</b> flavors, not just the currently selected flavor. We can offer APIs to iterate * through all available {@link ResourceFolderRepository} instances, not just the set of instances for * the current module's current flavor. This will allow us to for example preview the string translations * for a given resource name not just for the current flavor but for all other flavors as well. * </p> */ @SuppressWarnings("deprecation") // Deprecated com.android.util.Pair is required by ProjectCallback interface public abstract class LocalResourceRepository extends AbstractResourceRepository implements Disposable, ModificationTracker { protected static final Logger LOG = Logger.getInstance(LocalResourceRepository.class); private final String myDisplayName; @Nullable private List<MultiResourceRepository> myParents; protected long myGeneration; protected LocalResourceRepository(@NotNull String displayName) { super(false); myDisplayName = displayName; } @NotNull public String getDisplayName() { return myDisplayName; } @Override public void dispose() { } @Override public final boolean isFramework() { return false; } public void addParent(@NonNull MultiResourceRepository parent) { if (myParents == null) { myParents = Lists.newArrayListWithExpectedSize(2); // Don't expect many parents } myParents.add(parent); } public void removeParent(@NonNull MultiResourceRepository parent) { if (myParents != null) { myParents.remove(parent); } } protected void invalidateItemCaches(@Nullable ResourceType... types) { if (myParents != null) { for (MultiResourceRepository parent : myParents) { parent.invalidateCache(this, types); } } } // ---- Implements ModificationCount ---- /** * Returns the current generation of the app resources. Any time the app resources are updated, * the generation increases. This can be used to force refreshing of layouts etc (which will cache * configured app resources) when the project resources have changed since last render. * <p> * Note that the generation is not a simple change count. If you change the contents of a layout drawable XML file, * that will not affect the {@link ResourceItem} and {@link ResourceValue} results returned from * this repository; we only store the presence of file based resources like layouts, menus, and drawables. * Therefore, only additions or removals of these files will cause a generation change. * <p> * Value resource files, such as string files, will cause generation changes when they are edited (unless * the change is determined to not be relevant to resource values, such as a change in an XML comment, etc. * * @return the generation id */ @Override public long getModificationCount() { return myGeneration; } @Nullable public VirtualFile getMatchingFile(@NonNull VirtualFile file, @NonNull ResourceType type, @NonNull FolderConfiguration config) { @SuppressWarnings("deprecation") ResourceFile best = super.getMatchingFile(ResourceHelper.getResourceName(file), type, config); if (best != null) { if (best instanceof PsiResourceFile) { PsiResourceFile prf = (PsiResourceFile)best; return prf.getPsiFile().getVirtualFile(); } return LocalFileSystem.getInstance().findFileByIoFile(best.getFile()); } return null; } /** @deprecated Use {@link #getMatchingFile(VirtualFile, ResourceType, FolderConfiguration)} in the plugin code */ @Nullable @Override @Deprecated public ResourceFile getMatchingFile(@NonNull String name, @NonNull ResourceType type, @NonNull FolderConfiguration config) { assert name.indexOf('.') == -1 : name; return super.getMatchingFile(name, type, config); } @VisibleForTesting public boolean isScanPending(@NonNull PsiFile psiFile) { return false; } /** Returns the {@link PsiFile} corresponding to the source of the given resource item, if possible */ @Nullable public static PsiFile getItemPsiFile(@NonNull Project project, @NonNull ResourceItem item) { if (item instanceof PsiResourceItem) { PsiResourceItem psiResourceItem = (PsiResourceItem)item; return psiResourceItem.getPsiFile(); } ResourceFile source = item.getSource(); assert source != null : item.getName(); if (source instanceof PsiResourceFile) { PsiResourceFile prf = (PsiResourceFile)source; return prf.getPsiFile(); } File file = source.getFile(); VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file); if (virtualFile != null) { PsiManager psiManager = PsiManager.getInstance(project); return psiManager.findFile(virtualFile); } return null; } /** * Returns the {@link XmlTag} corresponding to the given resource item. This is only * defined for resource items in value files. */ @Nullable public static XmlTag getItemTag(@NonNull Project project, @NonNull ResourceItem item) { if (item instanceof PsiResourceItem) { PsiResourceItem psiResourceItem = (PsiResourceItem)item; return psiResourceItem.getTag(); } PsiFile psiFile = getItemPsiFile(project, item); if (psiFile instanceof XmlFile) { String resourceName = item.getName(); XmlFile xmlFile = (XmlFile)psiFile; ApplicationManager.getApplication().assertReadAccessAllowed(); XmlTag rootTag = xmlFile.getRootTag(); if (rootTag != null && rootTag.isValid()) { XmlTag[] subTags = rootTag.getSubTags(); for (XmlTag tag : subTags) { if (tag.isValid() && resourceName.equals(tag.getAttributeValue(SdkConstants.ATTR_NAME))) { return tag; } } } // This method should only be called on value resource types assert FolderTypeRelationship.getRelatedFolders(item.getType()).contains(ResourceFolderType.VALUES) : item.getType(); } return null; } @Nullable public String getViewTag(ResourceItem item) { if (item instanceof PsiResourceItem) { PsiResourceItem psiItem = (PsiResourceItem)item; XmlTag tag = psiItem.getTag(); if (tag != null && tag.isValid()) { return tag.getName(); } final String id = item.getName(); PsiFile file = psiItem.getPsiFile(); if (file.isValid() && file instanceof XmlFile) { XmlFile xmlFile = (XmlFile)file; XmlTag rootTag = xmlFile.getRootTag(); if (rootTag != null && rootTag.isValid()) { return findViewTag(rootTag, id); } } } return null; } @Nullable private static String findViewTag(XmlTag tag, String target) { String id = tag.getAttributeValue(ATTR_ID, ANDROID_URI); if (id != null && id.endsWith(target) && target.equals(LintUtils.stripIdPrefix(id))) { return tag.getName(); } for (XmlTag sub : tag.getSubTags()) { if (sub.isValid()) { String found = findViewTag(sub, target); if (found != null) { return found; } } } return null; } }