| /* |
| * 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.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| 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) { |
| List<VirtualFile> matches = getMatchingFiles(file, type, config); |
| return matches.isEmpty() ? null : matches.get(0); |
| } |
| |
| @NonNull |
| public List<VirtualFile> getMatchingFiles(@NonNull VirtualFile file, @NonNull ResourceType type, @NonNull FolderConfiguration config) { |
| List<ResourceFile> matches = super.getMatchingFiles(ResourceHelper.getResourceName(file), type, config); |
| List<VirtualFile> matchesFiles = new ArrayList<VirtualFile>(matches.size()); |
| for (ResourceFile match : matches) { |
| if (match != null) { |
| if (match instanceof PsiResourceFile) { |
| matchesFiles.add(((PsiResourceFile)match).getPsiFile().getVirtualFile()); |
| } |
| else { |
| matchesFiles.add(LocalFileSystem.getInstance().findFileByIoFile(match.getFile())); |
| } |
| } |
| } |
| return matchesFiles; |
| } |
| |
| /** @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); |
| } |
| |
| @Nullable |
| public DataBindingInfo getDataBindingInfoForLayout(String layoutName) { |
| return null; |
| } |
| |
| @Nullable |
| public Map<String, DataBindingInfo> getDataBindingResourceFiles() { |
| return null; |
| } |
| |
| @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(); |
| if (source == null) { // most likely a dynamically defined value |
| return null; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Forces the repository to update itself synchronously, if necessary (in case there |
| * are pending updates). This method must be called on the event dispatch thread! |
| */ |
| public void sync() { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| } |
| } |