| /* |
| * 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 org.jetbrains.android.facet; |
| |
| import com.android.tools.idea.gradle.IdeaAndroidProject; |
| import com.android.tools.idea.gradle.project.GradleSyncListener; |
| import com.android.tools.idea.gradle.variant.view.BuildVariantView; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Lists; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.roots.*; |
| import com.intellij.openapi.util.ModificationTracker; |
| import com.intellij.openapi.vfs.VfsUtilCore; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.vfs.VirtualFileManager; |
| import com.intellij.util.containers.hash.HashSet; |
| import org.jetbrains.android.maven.AndroidMavenUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.tools.idea.gradle.variant.view.BuildVariantView.BuildVariantSelectionChangeListener; |
| |
| /** |
| * The resource folder manager is responsible for returning the current set |
| * of resource folders used in the project. It provides hooks for getting notified |
| * when the set of folders changes (e.g. due to variant selection changes, or |
| * the folder set changing due to the user editing the gradle files or after a |
| * delayed project initialization), and it also provides some state caching between |
| * IDE sessions such that before the gradle initialization is done, it returns |
| * the folder set as it was before the IDE exited. |
| */ |
| public class ResourceFolderManager implements ModificationTracker { |
| public static final String EXPLODED_BUNDLES = "exploded-bundles"; |
| public static final String EXPLODED_AAR = "exploded-aar"; |
| |
| private final AndroidFacet myFacet; |
| private List<VirtualFile> myResDirCache; |
| private long myGeneration; |
| private final List<ResourceFolderListener> myListeners = Lists.newArrayList(); |
| private boolean myVariantListenerAdded; |
| private boolean myGradleInitListenerAdded; |
| |
| /** |
| * Should only be constructed by {@link AndroidFacet}; others should obtain instance |
| * via {@link AndroidFacet#getResourceFolderManager} |
| */ |
| ResourceFolderManager(AndroidFacet facet) { |
| myFacet = facet; |
| } |
| |
| /** Notifies the resource folder manager that the resource folder set may have changed */ |
| public void invalidate() { |
| List<VirtualFile> old = myResDirCache; |
| myResDirCache = null; |
| getFolders(); // sets myResDirCache as a side effect |
| //noinspection ConstantConditions |
| if (!old.equals(myResDirCache)) { |
| notifyChanged(old, myResDirCache); |
| } |
| } |
| |
| /** |
| * Returns all resource directories, in the overlay order |
| * <p> |
| * TODO: This should be changed to be a {@code List<List<VirtualFile>>} in order to be |
| * able to distinguish overlays (e.g. flavor directories) versus resource folders at |
| * the same level where duplicates are NOT allowed: [[flavor1], [flavor2], [main1,main2]] |
| * |
| * @return a list of all resource directories |
| */ |
| @NotNull |
| public List<VirtualFile> getFolders() { |
| if (myResDirCache == null) { |
| myResDirCache = computeFolders(); |
| } |
| |
| return myResDirCache; |
| } |
| |
| private List<VirtualFile> computeFolders() { |
| if (myFacet.requiresAndroidModel()) { |
| JpsAndroidModuleProperties state = myFacet.getConfiguration().getState(); |
| IdeaAndroidProject androidModel = myFacet.getAndroidModel(); |
| List<VirtualFile> resDirectories = new ArrayList<VirtualFile>(); |
| if (androidModel == null) { |
| // Read string property |
| if (state != null) { |
| String path = state.RES_FOLDERS_RELATIVE_PATH; |
| if (path != null) { |
| VirtualFileManager manager = VirtualFileManager.getInstance(); |
| // Deliberately using ';' instead of File.pathSeparator; see comment later in code below which |
| // writes the property |
| for (String url : Splitter.on(';').omitEmptyStrings().trimResults().split(path)) { |
| VirtualFile dir = manager.findFileByUrl(url); |
| if (dir != null) { |
| resDirectories.add(dir); |
| } |
| } |
| } else { |
| // First time; have not yet computed the res folders |
| // just try the default: src/main/res/ (from Gradle templates), res/ (from exported Eclipse projects) |
| String mainRes = '/' + FD_SOURCES + '/' + FD_MAIN + '/' + FD_RES; |
| VirtualFile dir = AndroidRootUtil.getFileByRelativeModulePath(myFacet.getModule(), mainRes, true); |
| if (dir != null) { |
| resDirectories.add(dir); |
| } else { |
| String res = '/' + FD_RES; |
| dir = AndroidRootUtil.getFileByRelativeModulePath(myFacet.getModule(), res, true); |
| if (dir != null) { |
| resDirectories.add(dir); |
| } |
| } |
| } |
| } |
| } else { |
| for (IdeaSourceProvider provider : IdeaSourceProvider.getCurrentSourceProviders(myFacet)) { |
| resDirectories.addAll(provider.getResDirectories()); |
| } |
| |
| // Write string property such that subsequent restarts can look up the most recent list |
| // before the gradle model has been initialized asynchronously |
| if (state != null) { |
| StringBuilder path = new StringBuilder(400); |
| for (VirtualFile dir : resDirectories) { |
| if (path.length() != 0) { |
| // Deliberately using ';' instead of File.pathSeparator since on Unix File.pathSeparator is ":" |
| // which is also used in URLs, meaning we could end up with something like "file://foo:file://bar" |
| path.append(';'); |
| } |
| path.append(dir.getUrl()); |
| } |
| state.RES_FOLDERS_RELATIVE_PATH = path.toString(); |
| } |
| |
| // Also refresh the app resources whenever the variant changes |
| if (!myVariantListenerAdded) { |
| myVariantListenerAdded = true; |
| BuildVariantView.getInstance(myFacet.getModule().getProject()).addListener(new BuildVariantSelectionChangeListener() { |
| @Override |
| public void buildVariantsConfigChanged() { |
| invalidate(); |
| } |
| }); |
| } |
| } |
| |
| // Add notification listener for when the project is initialized so we can update the |
| // resource set, if necessary |
| if (!myGradleInitListenerAdded) { |
| myGradleInitListenerAdded = true; // Avoid adding multiple listeners if we invalidate and call this repeatedly around startup |
| myFacet.addListener(new GradleSyncListener.Adapter() { |
| @Override |
| public void syncSucceeded(@NotNull Project project) { |
| // Resource folders can change on sync |
| invalidate(); |
| } |
| }); |
| } |
| |
| return resDirectories; |
| } else { |
| return new ArrayList<VirtualFile>(myFacet.getMainIdeaSourceProvider().getResDirectories()); |
| } |
| } |
| |
| private void notifyChanged(@NotNull List<VirtualFile> before, @NotNull List<VirtualFile> after) { |
| myGeneration++; |
| Set<VirtualFile> added = new HashSet<VirtualFile>(after.size()); |
| added.addAll(after); |
| added.removeAll(before); |
| |
| Set<VirtualFile> removed = new HashSet<VirtualFile>(before.size()); |
| removed.addAll(before); |
| removed.removeAll(after); |
| |
| for (ResourceFolderListener listener : new ArrayList<ResourceFolderListener>(myListeners)) { |
| listener.resourceFoldersChanged(myFacet, after, added, removed); |
| } |
| } |
| |
| @Override |
| public long getModificationCount() { |
| return myGeneration; |
| } |
| |
| public synchronized void addListener(@NotNull ResourceFolderListener listener) { |
| myListeners.add(listener); |
| } |
| |
| public synchronized void removeListener(@NotNull ResourceFolderListener listener) { |
| myListeners.remove(listener); |
| } |
| |
| /** Adds in any AAR library resource directories found in the library definitions for the given facet */ |
| public static void addAarsFromModuleLibraries(@NotNull AndroidFacet facet, @NotNull Set<File> dirs) { |
| Module module = facet.getModule(); |
| OrderEntry[] orderEntries = ModuleRootManager.getInstance(module).getOrderEntries(); |
| for (OrderEntry orderEntry : orderEntries) { |
| if (orderEntry instanceof LibraryOrSdkOrderEntry) { |
| if (orderEntry.isValid() && isAarDependency(facet, orderEntry)) { |
| final LibraryOrSdkOrderEntry entry = (LibraryOrSdkOrderEntry)orderEntry; |
| final VirtualFile[] libClasses = entry.getRootFiles(OrderRootType.CLASSES); |
| File res = null; |
| for (VirtualFile root : libClasses) { |
| if (root.getName().equals(FD_RES)) { |
| res = VfsUtilCore.virtualToIoFile(root); |
| break; |
| } |
| } |
| |
| if (res == null) { |
| for (VirtualFile root : libClasses) { |
| // Switch to file IO: The root may be inside a jar file system, where |
| // getParent() returns null (and to get the real parent is ugly; |
| // e.g. ((PersistentFSImpl.JarRoot)root).getParentLocalFile()). |
| // Besides, we need the java.io.File at the end of this anyway. |
| File file = new File(VfsUtilCore.virtualToIoFile(root).getParentFile(), FD_RES); |
| if (file.exists()) { |
| res = file; |
| break; |
| } |
| } |
| } |
| |
| if (res != null) { |
| dirs.add(res); |
| } |
| } |
| } |
| } |
| } |
| |
| private static boolean isAarDependency(@NotNull AndroidFacet facet, @NotNull OrderEntry orderEntry) { |
| if (facet.requiresAndroidModel() && orderEntry instanceof LibraryOrderEntry) { |
| VirtualFile[] files = orderEntry.getFiles(OrderRootType.CLASSES); |
| if (files.length >= 2) { |
| for (VirtualFile file : files) { |
| if (FD_RES.equals(file.getName()) && file.isDirectory()) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| return AndroidMavenUtil.isMavenAarDependency(facet.getModule(), orderEntry); |
| } |
| |
| /** |
| * Returns true if the given resource file (such as a given layout XML file) is an extracted library (AAR) resource file |
| * |
| * @param file the file to check |
| * @return true if the file is a library resource file |
| */ |
| public static boolean isLibraryResourceFile(@Nullable VirtualFile file) { |
| if (file != null) { |
| return isLibraryResourceFolder(file.getParent()); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns true if the given resource folder (such as a given "layout") is an extracted library (AAR) resource folder |
| * |
| * @param folder the folder to check |
| * @return true if the folder is a library resource folder |
| */ |
| public static boolean isLibraryResourceFolder(@Nullable VirtualFile folder) { |
| if (folder != null) { |
| return isLibraryResourceRoot(folder.getParent()); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns true if the given resource folder (such as a given "res" folder, a parent of say a layout folder) is an extracted |
| * library (AAR) resource folder |
| * |
| * @param res the folder to check |
| * @return true if the folder is a library resource folder |
| */ |
| public static boolean isLibraryResourceRoot(@Nullable VirtualFile res) { |
| if (res != null) { |
| VirtualFile aar = res.getParent(); |
| if (aar != null) { |
| VirtualFile exploded = aar.getParent(); |
| if (exploded != null) { |
| String name = exploded.getName(); |
| if (name.equals(EXPLODED_BUNDLES) || name.equals(EXPLODED_AAR)) { |
| return true; |
| } |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** Listeners for resource folder changes */ |
| public interface ResourceFolderListener { |
| /** The resource folders in this project has changed */ |
| void resourceFoldersChanged(@NotNull AndroidFacet facet, |
| @NotNull List<VirtualFile> folders, |
| @NotNull Collection<VirtualFile> added, |
| @NotNull Collection<VirtualFile> removed); |
| } |
| } |