diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager')
7 files changed, 2470 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java new file mode 100644 index 000000000..ab5ae4070 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.IntArrayWrapper; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; +import com.android.resources.ResourceType; +import com.android.util.Pair; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * A monitor for the compiled resources. This only monitors changes in the resources of type + * {@link ResourceType#ID}. + */ +public final class CompiledResourcesMonitor implements IFileListener, IProjectListener { + + private final static CompiledResourcesMonitor sThis = new CompiledResourcesMonitor(); + + /** + * Sets up the monitoring system. + * @param monitor The main Resource Monitor. + */ + public static void setupMonitor(GlobalProjectMonitor monitor) { + monitor.addFileListener(sThis, IResourceDelta.ADDED | IResourceDelta.CHANGED); + monitor.addProjectListener(sThis); + } + + /** + * private constructor to prevent construction. + */ + private CompiledResourcesMonitor() { + } + + + /* (non-Javadoc) + * Sent when a file changed : if the file is the R class, then it is parsed again to update + * the internal data. + * + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * + * @see IFileListener#fileChanged + */ + @Override + public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (!isAndroidProject || flags == IResourceDelta.MARKERS) { + // Not Android or only the markers changed: not relevant + return; + } + + IProject project = file.getProject(); + + if (file.getName().equals(SdkConstants.FN_COMPILED_RESOURCE_CLASS)) { + // create the classname + String className = getRClassName(project); + if (className == null) { + // We need to abort. + AdtPlugin.log(IStatus.ERROR, + "fileChanged: failed to find manifest package for project %1$s", //$NON-NLS-1$ + project.getName()); + return; + } + // path will begin with /projectName/bin/classes so we'll ignore that + IPath relativeClassPath = file.getFullPath().removeFirstSegments(3); + if (packagePathMatches(relativeClassPath.toString(), className)) { + loadAndParseRClass(project, className); + } + } + } + + /** + * Check to see if the package section of the given path matches the packageName. + * For example, /project/bin/classes/com/foo/app/R.class should match com.foo.app.R + * @param path the pathname of the file to look at + * @param packageName the package qualified name of the class + * @return true if the package section of the path matches the package qualified name + */ + private boolean packagePathMatches(String path, String packageName) { + // First strip the ".class" off the end of the path + String pathWithoutExtension = path.substring(0, path.indexOf(SdkConstants.DOT_CLASS)); + + // then split the components of each path by their separators + String [] pathArray = pathWithoutExtension.split(Pattern.quote(File.separator)); + String [] packageArray = packageName.split(AdtConstants.RE_DOT); + + + int pathIndex = 0; + int packageIndex = 0; + + while (pathIndex < pathArray.length && packageIndex < packageArray.length) { + if (pathArray[pathIndex].equals(packageArray[packageIndex]) == false) { + return false; + } + pathIndex++; + packageIndex++; + } + // We may have matched all the way up to this point, but we're not sure it's a match + // unless BOTH paths done + return (pathIndex == pathArray.length && packageIndex == packageArray.length); + } + + /** + * Processes project close event. + */ + @Override + public void projectClosed(IProject project) { + // the ProjectResources object will be removed by the ResourceManager. + } + + /** + * Processes project delete event. + */ + @Override + public void projectDeleted(IProject project) { + // the ProjectResources object will be removed by the ResourceManager. + } + + /** + * Processes project open event. + */ + @Override + public void projectOpened(IProject project) { + // when the project is opened, we get an ADDED event for each file, so we don't + // need to do anything here. + } + + @Override + public void projectRenamed(IProject project, IPath from) { + // renamed projects also trigger delete/open event, + // so nothing to be done here. + } + + /** + * Processes existing project at init time. + */ + @Override + public void projectOpenedWithWorkspace(IProject project) { + try { + // check this is an android project + if (project.hasNature(AdtConstants.NATURE_DEFAULT)) { + String className = getRClassName(project); + // Find the classname + if (className == null) { + // We need to abort. + AdtPlugin.log(IStatus.ERROR, + "projectOpenedWithWorkspace: failed to find manifest package for project %1$s", //$NON-NLS-1$ + project.getName()); + return; + } + loadAndParseRClass(project, className); + } + } catch (CoreException e) { + // pass + } + } + + @Override + public void allProjectsOpenedWithWorkspace() { + // nothing to do. + } + + + private void loadAndParseRClass(IProject project, String className) { + try { + // first check there's a ProjectResources to store the content + ProjectResources projectResources = ResourceManager.getInstance().getProjectResources( + project); + + if (projectResources != null) { + // create a temporary class loader to load the class + ProjectClassLoader loader = new ProjectClassLoader(null /* parentClassLoader */, + project); + + try { + Class<?> clazz = loader.loadClass(className); + + if (clazz != null) { + // create the maps to store the result of the parsing + Map<ResourceType, Map<String, Integer>> resourceValueMap = + new EnumMap<ResourceType, Map<String, Integer>>(ResourceType.class); + Map<Integer, Pair<ResourceType, String>> genericValueToNameMap = + new HashMap<Integer, Pair<ResourceType, String>>(); + Map<IntArrayWrapper, String> styleableValueToNameMap = + new HashMap<IntArrayWrapper, String>(); + + // parse the class + if (parseClass(clazz, genericValueToNameMap, styleableValueToNameMap, + resourceValueMap)) { + // now we associate the maps to the project. + projectResources.setCompiledResources(genericValueToNameMap, + styleableValueToNameMap, resourceValueMap); + } + } + } catch (Error e) { + // Log this error with the class name we're trying to load and abort. + AdtPlugin.log(e, "loadAndParseRClass failed to find class %1$s", className); //$NON-NLS-1$ + } + } + } catch (ClassNotFoundException e) { + // pass + } + } + + /** + * Parses a R class, and fills maps. + * @param rClass the class to parse + * @param genericValueToNameMap + * @param styleableValueToNameMap + * @param resourceValueMap + * @return True if we managed to parse the R class. + */ + private boolean parseClass(Class<?> rClass, + Map<Integer, Pair<ResourceType, String>> genericValueToNameMap, + Map<IntArrayWrapper, String> styleableValueToNameMap, Map<ResourceType, + Map<String, Integer>> resourceValueMap) { + try { + for (Class<?> inner : rClass.getDeclaredClasses()) { + String resTypeName = inner.getSimpleName(); + ResourceType resType = ResourceType.getEnum(resTypeName); + + if (resType != null) { + Map<String, Integer> fullMap = new HashMap<String, Integer>(); + resourceValueMap.put(resType, fullMap); + + for (Field f : inner.getDeclaredFields()) { + // only process static final fields. + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers)) { + Class<?> type = f.getType(); + if (type.isArray() && type.getComponentType() == int.class) { + // if the object is an int[] we put it in the styleable map + styleableValueToNameMap.put( + new IntArrayWrapper((int[]) f.get(null)), + f.getName()); + } else if (type == int.class) { + Integer value = (Integer) f.get(null); + genericValueToNameMap.put(value, Pair.of(resType, f.getName())); + fullMap.put(f.getName(), value); + } else { + assert false; + } + } + } + } + } + + return true; + } catch (IllegalArgumentException e) { + } catch (IllegalAccessException e) { + } + return false; + } + + /** + * Returns the class name of the R class, based on the project's manifest's package. + * + * @return A class name (e.g. "my.app.R") or null if there's no valid package in the manifest. + */ + private String getRClassName(IProject project) { + IFile manifestFile = ProjectHelper.getManifest(project); + if (manifestFile != null && manifestFile.isSynchronized(IResource.DEPTH_ZERO)) { + ManifestData data = AndroidManifestHelper.parseForData(manifestFile); + if (data != null) { + String javaPackage = data.getPackage(); + return javaPackage + ".R"; //$NON-NLS-1$ + } + } + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java new file mode 100644 index 000000000..7bab4fd54 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import com.android.resources.ResourceType; +import com.android.util.Pair; +import com.android.utils.SparseArray; + +import java.util.HashMap; +import java.util.Map; + +public class DynamicIdMap { + + private final Map<Pair<ResourceType, String>, Integer> mDynamicIds = new HashMap<Pair<ResourceType, String>, Integer>(); + private final SparseArray<Pair<ResourceType, String>> mRevDynamicIds = new SparseArray<Pair<ResourceType, String>>(); + private int mDynamicSeed; + + public DynamicIdMap(int seed) { + mDynamicSeed = seed; + } + + public void reset(int seed) { + mDynamicIds.clear(); + mRevDynamicIds.clear(); + mDynamicSeed = seed; + } + + /** + * Returns a dynamic integer for the given resource type/name, creating it if it doesn't + * already exist. + * + * @param type the type of the resource + * @param name the name of the resource + * @return an integer. + */ + public Integer getId(ResourceType type, String name) { + return getId(Pair.of(type, name)); + } + + /** + * Returns a dynamic integer for the given resource type/name, creating it if it doesn't + * already exist. + * + * @param resource the type/name of the resource + * @return an integer. + */ + public Integer getId(Pair<ResourceType, String> resource) { + Integer value = mDynamicIds.get(resource); + if (value == null) { + value = Integer.valueOf(++mDynamicSeed); + mDynamicIds.put(resource, value); + mRevDynamicIds.put(value, resource); + } + + return value; + } + + public Pair<ResourceType, String> resolveId(int id) { + return mRevDynamicIds.get(id); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java new file mode 100644 index 000000000..674a601d0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java @@ -0,0 +1,546 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.resources.ResourceFolder; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.util.ArrayList; + +/** + * The Global Project Monitor tracks project file changes, and forward them to simple project, + * file, and folder listeners. + * Those listeners can be setup with masks to listen to particular events. + * <p/> + * To track project resource changes, use the monitor in the {@link ResourceManager}. It is more + * efficient and while the global ProjectMonitor can track any file, deleted resource files + * cannot be matched to previous {@link ResourceFile} or {@link ResourceFolder} objects by the + * time the listeners get the event notifications. + * + * @see IProjectListener + * @see IFolderListener + * @see IFileListener + */ +public final class GlobalProjectMonitor { + + private final static GlobalProjectMonitor sThis = new GlobalProjectMonitor(); + + /** + * Classes which implement this interface provide a method that deals + * with file change events. + */ + public interface IFileListener { + /** + * Sent when a file changed. + * + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * @param extension the extension of the file or null if the file does + * not have an extension + * @param flags the {@link IResourceDelta#getFlags()} value with details + * on what changed in the file + * @param isAndroidProject whether the parent project is an Android Project + */ + public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, + int kind, @Nullable String extension, int flags, boolean isAndroidProject); + } + + /** + * Classes which implements this interface provide methods dealing with project events. + */ + public interface IProjectListener { + /** + * Sent for each opened android project at the time the listener is put in place. + * @param project the opened project. + */ + public void projectOpenedWithWorkspace(IProject project); + + /** + * Sent once after all Android projects have been opened, + * at the time the listener is put in place. + * <p/> + * This is called after {@link #projectOpenedWithWorkspace(IProject)} has + * been called on all known Android projects. + */ + public void allProjectsOpenedWithWorkspace(); + + /** + * Sent when a project is opened. + * @param project the project being opened. + */ + public void projectOpened(IProject project); + + /** + * Sent when a project is closed. + * @param project the project being closed. + */ + public void projectClosed(IProject project); + + /** + * Sent when a project is deleted. + * @param project the project about to be deleted. + */ + public void projectDeleted(IProject project); + + /** + * Sent when a project is renamed. During a project rename + * {@link #projectDeleted(IProject)} and {@link #projectOpened(IProject)} are also called. + * This is called last. + * + * @param project the new {@link IProject} object. + * @param from the path of the project before the rename action. + */ + public void projectRenamed(IProject project, IPath from); + } + + /** + * Classes which implement this interface provide a method that deals + * with folder change events + */ + public interface IFolderListener { + /** + * Sent when a folder changed. + * @param folder The file that was changed + * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()} + * @param isAndroidProject whether the parent project is an Android Project + */ + public void folderChanged(IFolder folder, int kind, boolean isAndroidProject); + } + + /** + * Interface for a listener to be notified when resource change event starts and ends. + */ + public interface IResourceEventListener { + public void resourceChangeEventStart(); + public void resourceChangeEventEnd(); + } + + /** + * Interface for a listener that gets passed the raw delta without processing. + */ + public interface IRawDeltaListener { + public void visitDelta(IResourceDelta delta); + } + + /** + * Base listener bundle to associate a listener to an event mask. + */ + private static class ListenerBundle { + /** Mask value to accept all events */ + public final static int MASK_NONE = -1; + + /** + * Event mask. Values accepted are IResourceDelta.### + * @see IResourceDelta#ADDED + * @see IResourceDelta#REMOVED + * @see IResourceDelta#CHANGED + * @see IResourceDelta#ADDED_PHANTOM + * @see IResourceDelta#REMOVED_PHANTOM + * */ + int kindMask; + } + + /** + * Listener bundle for file event. + */ + private static class FileListenerBundle extends ListenerBundle { + + /** The file listener */ + IFileListener listener; + } + + /** + * Listener bundle for folder event. + */ + private static class FolderListenerBundle extends ListenerBundle { + /** The file listener */ + IFolderListener listener; + } + + private final ArrayList<FileListenerBundle> mFileListeners = + new ArrayList<FileListenerBundle>(); + + private final ArrayList<FolderListenerBundle> mFolderListeners = + new ArrayList<FolderListenerBundle>(); + + private final ArrayList<IProjectListener> mProjectListeners = new ArrayList<IProjectListener>(); + + private final ArrayList<IResourceEventListener> mEventListeners = + new ArrayList<IResourceEventListener>(); + + private final ArrayList<IRawDeltaListener> mRawDeltaListeners = + new ArrayList<IRawDeltaListener>(); + + private IWorkspace mWorkspace; + + private boolean mIsAndroidProject; + + /** + * Delta visitor for resource changes. + */ + private final class DeltaVisitor implements IResourceDeltaVisitor { + + @Override + public boolean visit(IResourceDelta delta) { + // Find the other resource listeners to notify + IResource r = delta.getResource(); + int type = r.getType(); + if (type == IResource.FILE) { + int kind = delta.getKind(); + // notify the listeners. + for (FileListenerBundle bundle : mFileListeners) { + if (bundle.kindMask == ListenerBundle.MASK_NONE + || (bundle.kindMask & kind) != 0) { + try { + bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind, + r.getFileExtension(), delta.getFlags(), mIsAndroidProject); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IFileListener.fileChanged"); + } + } + } + return false; + } else if (type == IResource.FOLDER) { + int kind = delta.getKind(); + // notify the listeners. + for (FolderListenerBundle bundle : mFolderListeners) { + if (bundle.kindMask == ListenerBundle.MASK_NONE + || (bundle.kindMask & kind) != 0) { + try { + bundle.listener.folderChanged((IFolder)r, kind, mIsAndroidProject); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IFileListener.folderChanged"); + } + } + } + return true; + } else if (type == IResource.PROJECT) { + IProject project = (IProject)r; + + try { + mIsAndroidProject = project.hasNature(AdtConstants.NATURE_DEFAULT); + } catch (CoreException e) { + // this can only happen if the project does not exist or is not open, neither + // of which can happen here since we are processing changes in the project + // or at worst a project post-open event. + return false; + } + + if (mIsAndroidProject == false) { + // for non android project, skip the project listeners but return true + // to visit the children and notify the IFileListeners + return true; + } + + int flags = delta.getFlags(); + + if ((flags & IResourceDelta.OPEN) != 0) { + // the project is opening or closing. + + if (project.isOpen()) { + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + try { + pl.projectOpened(project); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IProjectListener.projectOpened"); + } + } + } else { + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + try { + pl.projectClosed(project); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IProjectListener.projectClosed"); + } + } + } + + if ((flags & IResourceDelta.MOVED_FROM) != 0) { + IPath from = delta.getMovedFromPath(); + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + try { + pl.projectRenamed(project, from); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IProjectListener.projectRenamed"); + } + } + } + } + } + + return true; + } + } + + public static GlobalProjectMonitor getMonitor() { + return sThis; + } + + + /** + * Starts the resource monitoring. + * @param ws The current workspace. + * @return The monitor object. + */ + public static GlobalProjectMonitor startMonitoring(IWorkspace ws) { + if (sThis != null) { + ws.addResourceChangeListener(sThis.mResourceChangeListener, + IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE); + sThis.mWorkspace = ws; + } + return sThis; + } + + /** + * Stops the resource monitoring. + * @param ws The current workspace. + */ + public static void stopMonitoring(IWorkspace ws) { + if (sThis != null) { + ws.removeResourceChangeListener(sThis.mResourceChangeListener); + + synchronized (sThis) { + sThis.mFileListeners.clear(); + sThis.mProjectListeners.clear(); + } + } + } + + /** + * Adds a file listener. + * @param listener The listener to receive the events. + * @param kindMask The event mask to filter out specific events. + * {@link ListenerBundle#MASK_NONE} will forward all events. + * See {@link ListenerBundle#kindMask} for more values. + */ + public synchronized void addFileListener(IFileListener listener, int kindMask) { + FileListenerBundle bundle = new FileListenerBundle(); + bundle.listener = listener; + bundle.kindMask = kindMask; + + mFileListeners.add(bundle); + } + + /** + * Removes an existing file listener. + * @param listener the listener to remove. + */ + public synchronized void removeFileListener(IFileListener listener) { + for (int i = 0 ; i < mFileListeners.size() ; i++) { + FileListenerBundle bundle = mFileListeners.get(i); + if (bundle.listener == listener) { + mFileListeners.remove(i); + return; + } + } + } + + /** + * Adds a folder listener. + * @param listener The listener to receive the events. + * @param kindMask The event mask to filter out specific events. + * {@link ListenerBundle#MASK_NONE} will forward all events. + * See {@link ListenerBundle#kindMask} for more values. + */ + public synchronized void addFolderListener(IFolderListener listener, int kindMask) { + FolderListenerBundle bundle = new FolderListenerBundle(); + bundle.listener = listener; + bundle.kindMask = kindMask; + + mFolderListeners.add(bundle); + } + + /** + * Removes an existing folder listener. + * @param listener the listener to remove. + */ + public synchronized void removeFolderListener(IFolderListener listener) { + for (int i = 0 ; i < mFolderListeners.size() ; i++) { + FolderListenerBundle bundle = mFolderListeners.get(i); + if (bundle.listener == listener) { + mFolderListeners.remove(i); + return; + } + } + } + + /** + * Adds a project listener. + * @param listener The listener to receive the events. + */ + public synchronized void addProjectListener(IProjectListener listener) { + mProjectListeners.add(listener); + + // we need to look at the opened projects and give them to the listener. + + // get the list of opened android projects. + IWorkspaceRoot workspaceRoot = mWorkspace.getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(javaModel, + null /*filter*/); + + + notifyResourceEventStart(); + + for (IJavaProject androidProject : androidProjects) { + listener.projectOpenedWithWorkspace(androidProject.getProject()); + } + + listener.allProjectsOpenedWithWorkspace(); + + notifyResourceEventEnd(); + } + + /** + * Removes an existing project listener. + * @param listener the listener to remove. + */ + public synchronized void removeProjectListener(IProjectListener listener) { + mProjectListeners.remove(listener); + } + + /** + * Adds a resource event listener. + * @param listener The listener to receive the events. + */ + public synchronized void addResourceEventListener(IResourceEventListener listener) { + mEventListeners.add(listener); + } + + /** + * Removes an existing Resource Event listener. + * @param listener the listener to remove. + */ + public synchronized void removeResourceEventListener(IResourceEventListener listener) { + mEventListeners.remove(listener); + } + + /** + * Adds a raw delta listener. + * @param listener The listener to receive the deltas. + */ + public synchronized void addRawDeltaListener(IRawDeltaListener listener) { + mRawDeltaListeners.add(listener); + } + + /** + * Removes an existing Raw Delta listener. + * @param listener the listener to remove. + */ + public synchronized void removeRawDeltaListener(IRawDeltaListener listener) { + mRawDeltaListeners.remove(listener); + } + + private void notifyResourceEventStart() { + for (IResourceEventListener listener : mEventListeners) { + try { + listener.resourceChangeEventStart(); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventStart"); + } + } + } + + private void notifyResourceEventEnd() { + for (IResourceEventListener listener : mEventListeners) { + try { + listener.resourceChangeEventEnd(); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventEnd"); + } + } + } + + private IResourceChangeListener mResourceChangeListener = new IResourceChangeListener() { + /** + * Processes the workspace resource change events. + * + * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent) + */ + @Override + public synchronized void resourceChanged(IResourceChangeEvent event) { + // notify the event listeners of a start. + notifyResourceEventStart(); + + if (event.getType() == IResourceChangeEvent.PRE_DELETE) { + // a project is being deleted. Lets get the project object and remove + // its compiled resource list. + IResource r = event.getResource(); + IProject project = r.getProject(); + + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT)) { + try { + pl.projectDeleted(project); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IProjectListener.projectDeleted"); + } + } + } catch (CoreException e) { + // just ignore this project. + } + } + } else { + // this a regular resource change. We get the delta and go through it with a visitor. + IResourceDelta delta = event.getDelta(); + + // notify the raw delta listeners + for (IRawDeltaListener listener : mRawDeltaListeners) { + listener.visitDelta(delta); + } + + DeltaVisitor visitor = new DeltaVisitor(); + try { + delta.accept(visitor); + } catch (CoreException e) { + } + } + + // we're done, notify the event listeners. + notifyResourceEventEnd(); + } + }; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java new file mode 100644 index 000000000..d61324937 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.ide.eclipse.adt.AdtConstants.MARKER_AAPT_COMPILE; +import static org.eclipse.core.resources.IResource.DEPTH_ONE; +import static org.eclipse.core.resources.IResource.DEPTH_ZERO; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ScanningContext; +import com.android.ide.common.resources.platform.AttributeInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.AaptParser; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An {@link IdeScanningContext} is a specialized {@link ScanningContext} which + * carries extra information about the scanning state, such as which file is + * currently being scanned, and which files have been scanned in the past, such + * that at the end of a scan we can mark and clear errors, etc. + */ +public class IdeScanningContext extends ScanningContext { + private final IProject mProject; + private final List<IResource> mScannedResources = new ArrayList<IResource>(); + private IResource mCurrentFile; + private List<Pair<IResource, String>> mErrors; + private Set<IProject> mFullAaptProjects; + private boolean mValidate; + private Map<String, AttributeInfo> mAttributeMap; + private ResourceRepository mFrameworkResources; + + /** + * Constructs a new {@link IdeScanningContext} + * + * @param repository the associated {@link ResourceRepository} + * @param project the associated project + * @param validate if true, check that the attributes and resources are + * valid and if not request a full AAPT check + */ + public IdeScanningContext(@NonNull ResourceRepository repository, @NonNull IProject project, + boolean validate) { + super(repository); + mProject = project; + mValidate = validate; + + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + AndroidTargetData targetData = sdk.getTargetData(project); + if (targetData != null) { + mAttributeMap = targetData.getAttributeMap(); + mFrameworkResources = targetData.getFrameworkResources(); + } + } + } + + @Override + public void addError(@NonNull String error) { + super.addError(error); + + if (mErrors == null) { + mErrors = new ArrayList<Pair<IResource,String>>(); + } + mErrors.add(Pair.of(mCurrentFile, error)); + } + + /** + * Notifies the context that the given resource is about to be scanned. + * + * @param resource the resource about to be scanned + */ + public void startScanning(@NonNull IResource resource) { + assert mCurrentFile == null : mCurrentFile; + mCurrentFile = resource; + mScannedResources.add(resource); + } + + /** + * Notifies the context that the given resource has been scanned. + * + * @param resource the resource that was scanned + */ + public void finishScanning(@NonNull IResource resource) { + assert mCurrentFile != null; + mCurrentFile = null; + } + + /** + * Process any errors found to add error markers in the affected files (and + * also clear up any aapt errors in files that are no longer applicable) + * + * @param async if true, delay updating markers until the next display + * thread event loop update + */ + public void updateMarkers(boolean async) { + // Run asynchronously? This is necessary for example when adding markers + // as the result of a resource change notification, since at that point the + // resource tree is locked for modifications and attempting to create a + // marker will throw a org.eclipse.core.internal.resources.ResourceException. + if (async) { + AdtPlugin.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + updateMarkers(false); + } + }); + return; + } + + // First clear out old/previous markers + for (IResource resource : mScannedResources) { + try { + if (resource.exists()) { + int depth = resource instanceof IFolder ? DEPTH_ONE : DEPTH_ZERO; + resource.deleteMarkers(MARKER_AAPT_COMPILE, true, depth); + } + } catch (CoreException ce) { + // Pass + } + } + + // Add new errors + if (mErrors != null && mErrors.size() > 0) { + List<String> errors = new ArrayList<String>(); + for (Pair<IResource, String> pair : mErrors) { + errors.add(pair.getSecond()); + } + AaptParser.parseOutput(errors, mProject); + } + } + + @Override + public boolean needsFullAapt() { + // returns true if it was explicitly requested or if a file that has errors was modified. + // This handles the case where an edit doesn't add any new id but fix a compile error. + return super.needsFullAapt() || hasModifiedFilesWithErrors(); + } + + /** + * Returns true if any of the scanned resources has an error marker on it. + */ + private boolean hasModifiedFilesWithErrors() { + for (IResource resource : mScannedResources) { + try { + int depth = resource instanceof IFolder ? DEPTH_ONE : DEPTH_ZERO; + if (resource.exists()) { + IMarker[] markers = resource.findMarkers(IMarker.PROBLEM, + true /*includeSubtypes*/, depth); + for (IMarker marker : markers) { + if (marker.getAttribute(IMarker.SEVERITY, IMarker.SEVERITY_INFO) == + IMarker.SEVERITY_ERROR) { + return true; + } + } + } + } catch (CoreException ce) { + // Pass + } + } + + return false; + } + + @Override + protected void requestFullAapt() { + super.requestFullAapt(); + + if (mCurrentFile != null) { + if (mFullAaptProjects == null) { + mFullAaptProjects = new HashSet<IProject>(); + } + mFullAaptProjects.add(mCurrentFile.getProject()); + } else { + assert false : "No current context to apply IdeScanningContext to"; + } + } + + /** + * Returns the collection of projects that scanned resources have requested + * a full aapt for. + * + * @return a collection of projects that scanned resources requested full + * aapt runs for, or null + */ + public Collection<IProject> getAaptRequestedProjects() { + return mFullAaptProjects; + } + + @Override + public boolean checkValue(@Nullable String uri, @NonNull String name, @NonNull String value) { + if (!mValidate) { + return true; + } + + if (!needsFullAapt() && mAttributeMap != null && ANDROID_URI.equals(uri)) { + AttributeInfo info = mAttributeMap.get(name); + if (info != null && !info.isValid(value, mRepository, mFrameworkResources)) { + return false; + } + } + + return super.checkValue(uri, name, value); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java new file mode 100644 index 000000000..e07f09927 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.BuildHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +/** + * ClassLoader able to load class from output of an Eclipse project. + */ +public final class ProjectClassLoader extends ClassLoader { + + private final IJavaProject mJavaProject; + private URLClassLoader mJarClassLoader; + private boolean mInsideJarClassLoader = false; + + public ProjectClassLoader(ClassLoader parentClassLoader, IProject project) { + super(parentClassLoader); + mJavaProject = JavaCore.create(project); + } + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + // if we are here through a child classloader, throw an exception. + if (mInsideJarClassLoader) { + throw new ClassNotFoundException(name); + } + + // attempt to load the class from the main project + Class<?> clazz = loadFromProject(mJavaProject, name); + + if (clazz != null) { + return clazz; + } + + // attempt to load the class from the jar dependencies + clazz = loadClassFromJar(name); + if (clazz != null) { + return clazz; + } + + // attempt to load the class from the libraries + try { + // get the project info + ProjectState projectState = Sdk.getProjectState(mJavaProject.getProject()); + + // this can happen if the project has no project.properties. + if (projectState != null) { + + List<IProject> libProjects = projectState.getFullLibraryProjects(); + List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects( + libProjects); + + for (IJavaProject javaProject : referencedJavaProjects) { + clazz = loadFromProject(javaProject, name); + + if (clazz != null) { + return clazz; + } + } + } + } catch (CoreException e) { + // log exception? + } + + throw new ClassNotFoundException(name); + } + + /** + * Attempts to load a class from a project output folder. + * @param project the project to load the class from + * @param name the name of the class + * @return a class object if found, null otherwise. + */ + private Class<?> loadFromProject(IJavaProject project, String name) { + try { + // get the project output folder. + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IPath outputLocation = project.getOutputLocation(); + IResource outRes = root.findMember(outputLocation); + if (outRes == null) { + return null; + } + + File outFolder = new File(outRes.getLocation().toOSString()); + + // get the class name segments + String[] segments = name.split("\\."); //$NON-NLS-1$ + + // try to load the class from the bin folder of the project. + File classFile = getFile(outFolder, segments, 0); + if (classFile == null) { + return null; + } + + // load the content of the file and create the class. + FileInputStream fis = new FileInputStream(classFile); + byte[] data = new byte[(int)classFile.length()]; + int read = 0; + try { + read = fis.read(data); + } catch (IOException e) { + data = null; + } + fis.close(); + + if (data != null) { + try { + Class<?> clazz = defineClass(null, data, 0, read); + if (clazz != null) { + return clazz; + } + } catch (UnsupportedClassVersionError e) { + // Attempt to reload on lower version + int maxVersion = 50; // JDK 1.6 + try { + byte[] rewritten = rewriteClass(data, maxVersion, 0); + return defineClass(null, rewritten, 0, rewritten.length); + } catch (UnsupportedClassVersionError e2) { + throw e; // throw *original* exception, not attempt to rewrite + } + } + } + } catch (Exception e) { + // log the exception? + } + + return null; + } + + /** + * Rewrites the given class to the given target class file version. + */ + public static byte[] rewriteClass(byte[] classData, final int maxVersion, final int minVersion) { + assert maxVersion >= minVersion; + ClassWriter classWriter = new ClassWriter(0); + ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) { + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + if (version > maxVersion) { + version = maxVersion; + } + if (version < minVersion) { + version = minVersion; + } + super.visit(version, access, name, signature, superName, interfaces); + } + }; + ClassReader reader = new ClassReader(classData); + reader.accept(classVisitor, 0); + return classWriter.toByteArray(); + } + + /** + * Returns the File matching the a certain path from a root {@link File}. + * <p/>The methods checks that the file ends in .class even though the last segment + * does not. + * @param parent the root of the file. + * @param segments the segments containing the path of the file + * @param index the offset at which to start looking into segments. + * @throws FileNotFoundException + */ + private File getFile(File parent, String[] segments, int index) + throws FileNotFoundException { + // reached the end with no match? + if (index == segments.length) { + throw new FileNotFoundException(); + } + + String toMatch = segments[index]; + File[] files = parent.listFiles(); + + // we're at the last segments. we look for a matching <file>.class + if (index == segments.length - 1) { + toMatch = toMatch + ".class"; + + if (files != null) { + for (File file : files) { + if (file.isFile() && file.getName().equals(toMatch)) { + return file; + } + } + } + + // no match? abort. + throw new FileNotFoundException(); + } + + String innerClassName = null; + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + if (toMatch.equals(file.getName())) { + return getFile(file, segments, index+1); + } + } else if (file.getName().startsWith(toMatch)) { + if (innerClassName == null) { + StringBuilder sb = new StringBuilder(segments[index]); + for (int i = index + 1 ; i < segments.length ; i++) { + sb.append('$'); + sb.append(segments[i]); + } + sb.append(".class"); + + innerClassName = sb.toString(); + } + + if (file.getName().equals(innerClassName)) { + return file; + } + } + } + } + + return null; + } + + /** + * Loads a class from the 3rd party jar present in the project + * + * @return the class loader or null if not found. + */ + private Class<?> loadClassFromJar(String name) { + if (mJarClassLoader == null) { + // get the OS path to all the external jars + URL[] jars = getExternalJars(); + + mJarClassLoader = new URLClassLoader(jars, this /* parent */); + } + + try { + // because a class loader always look in its parent loader first, we need to know + // that we are querying the jar classloader. This will let us know to not query + // it again for classes we don't find, or this would create an infinite loop. + mInsideJarClassLoader = true; + return mJarClassLoader.loadClass(name); + } catch (ClassNotFoundException e) { + // not found? return null. + return null; + } finally { + mInsideJarClassLoader = false; + } + } + + /** + * Returns an array of external jar files used by the project. + * @return an array of OS-specific absolute file paths + */ + private final URL[] getExternalJars() { + // get a java project from it + IJavaProject javaProject = JavaCore.create(mJavaProject.getProject()); + + ArrayList<URL> oslibraryList = new ArrayList<URL>(); + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY || + e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + // if this is a classpath variable reference, we resolve it. + if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + e = JavaCore.getResolvedClasspathEntry(e); + } + + handleClassPathEntry(e, oslibraryList); + } else if (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + // get the container. + try { + IClasspathContainer container = JavaCore.getClasspathContainer( + e.getPath(), javaProject); + // ignore the system and default_system types as they represent + // libraries that are part of the runtime. + if (container != null && + container.getKind() == IClasspathContainer.K_APPLICATION) { + IClasspathEntry[] entries = container.getClasspathEntries(); + for (IClasspathEntry entry : entries) { + // TODO: Xav -- is this necessary? + if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + entry = JavaCore.getResolvedClasspathEntry(entry); + } + + handleClassPathEntry(entry, oslibraryList); + } + } + } catch (JavaModelException jme) { + // can't resolve the container? ignore it. + AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", + e.getPath()); + } + } + } + } + + return oslibraryList.toArray(new URL[oslibraryList.size()]); + } + + private void handleClassPathEntry(IClasspathEntry e, ArrayList<URL> oslibraryList) { + // get the IPath + IPath path = e.getPath(); + + // check the name ends with .jar + if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + boolean local = false; + IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(path); + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + local = true; + try { + oslibraryList.add(new File(resource.getLocation().toOSString()) + .toURI().toURL()); + } catch (MalformedURLException mue) { + // pass + } + } + + if (local == false) { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.exists()) { + try { + oslibraryList.add(f.toURI().toURL()); + } catch (MalformedURLException mue) { + // pass + } + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java new file mode 100644 index 000000000..7c3fd4c13 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.IntArrayWrapper; +import com.android.ide.common.resources.ResourceFolder; +import com.android.ide.common.resources.ResourceItem; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.IAbstractFolder; +import com.android.resources.ResourceType; +import com.android.util.Pair; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Represents the resources of a project. + * On top of the regular {@link ResourceRepository} features it provides: + *<ul> + *<li>configured resources contain the resources coming from the libraries.</li> + *<li>resolution to and from resource integer (compiled value in R.java).</li> + *<li>handles resource integer for non existing values of type ID. This is used when rendering.</li> + *<li>layouts that have no been saved yet. This is handled by generating dynamic IDs + * on the fly.</li> + *</ul> + */ +@SuppressWarnings("deprecation") +public class ProjectResources extends ResourceRepository { + // project resources 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 final static int DYNAMIC_ID_SEED_START = 0x7fff0000; + + /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */ + private Map<ResourceType, Map<String, Integer>> mResourceValueMap; + /** Map of (id, [name, resType]) for all resources coming from R.java */ + private Map<Integer, Pair<ResourceType, String>> mResIdValueToNameMap; + /** Map of (int[], name) for styleable resources coming from R.java */ + private Map<IntArrayWrapper, String> mStyleableValueToNameMap; + + private final DynamicIdMap mDynamicIdMap = new DynamicIdMap(DYNAMIC_ID_SEED_START); + private final IntArrayWrapper mWrapper = new IntArrayWrapper(null); + private final IProject mProject; + + public static ProjectResources create(IProject project) { + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + return new ProjectResources(project, new IFolderWrapper(resFolder)); + } + + /** + * Makes a ProjectResources for a given <var>project</var>. + * @param project the project. + */ + private ProjectResources(IProject project, IAbstractFolder resFolder) { + super(resFolder, false /*isFrameworkRepository*/); + mProject = project; + } + + /** + * Returns the resources values matching a given {@link FolderConfiguration}, this will + * include library dependency. + * + * @param referenceConfig the configuration that each value must match. + * @return a map with guaranteed to contain an entry for each {@link ResourceType} + */ + @Override + @NonNull + public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources( + @NonNull FolderConfiguration referenceConfig) { + ensureInitialized(); + + Map<ResourceType, Map<String, ResourceValue>> resultMap = + new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class); + + // if the project contains libraries, we need to add the libraries resources here + // so that they are accessible to the layout rendering. + if (mProject != null) { + ProjectState state = Sdk.getProjectState(mProject); + if (state != null) { + List<IProject> libraries = state.getFullLibraryProjects(); + + ResourceManager resMgr = ResourceManager.getInstance(); + + // because aapt put all the library in their order in this array, the first + // one will have priority over the 2nd one. So it's better to loop in the inverse + // order and fill the map with resources that will be overwritten by higher + // priority resources + for (int i = libraries.size() - 1 ; i >= 0 ; i--) { + IProject library = libraries.get(i); + + ProjectResources libRes = resMgr.getProjectResources(library); + if (libRes != null) { + // get the library resources, and only the library, not the dependencies + // so call doGetConfiguredResources() directly. + Map<ResourceType, Map<String, ResourceValue>> libMap = + libRes.doGetConfiguredResources(referenceConfig); + + // we don't want to simply replace the whole map, but instead merge the + // content of any sub-map + for (Entry<ResourceType, Map<String, ResourceValue>> libEntry : + libMap.entrySet()) { + + // get the map currently in the result map for this resource type + Map<String, ResourceValue> tempMap = resultMap.get(libEntry.getKey()); + if (tempMap == null) { + // since there's no current map for this type, just add the map + // directly coming from the library resources + resultMap.put(libEntry.getKey(), libEntry.getValue()); + } else { + // already a map for this type. add the resources from the + // library, this will override existing value, which is why + // we loop in a specific library order. + tempMap.putAll(libEntry.getValue()); + } + } + } + } + } + } + + // now the project resources themselves. + Map<ResourceType, Map<String, ResourceValue>> thisProjectMap = + doGetConfiguredResources(referenceConfig); + + // now merge the maps. + for (Entry<ResourceType, Map<String, ResourceValue>> entry : thisProjectMap.entrySet()) { + ResourceType type = entry.getKey(); + Map<String, ResourceValue> typeMap = resultMap.get(type); + if (typeMap == null) { + resultMap.put(type, entry.getValue()); + } else { + typeMap.putAll(entry.getValue()); + } + } + + return resultMap; + } + + /** + * Returns the {@link ResourceFolder} associated with a {@link IFolder}. + * @param folder The {@link IFolder} object. + * @return the {@link ResourceFolder} or null if it was not found. + * + * @see ResourceRepository#getResourceFolder(com.android.io.IAbstractFolder) + */ + public ResourceFolder getResourceFolder(IFolder folder) { + return getResourceFolder(new IFolderWrapper(folder)); + } + + /** + * Resolves a compiled resource id into the resource name and type + * @param id the resource integer id. + * @return a {@link Pair} of 2 strings { name, type } or null if the id could not be resolved + */ + public Pair<ResourceType, String> resolveResourceId(int id) { + Pair<ResourceType, String> result = null; + if (mResIdValueToNameMap != null) { + result = mResIdValueToNameMap.get(id); + } + + if (result == null) { + synchronized (mDynamicIdMap) { + result = mDynamicIdMap.resolveId(id); + } + } + + return result; + } + + /** + * Resolves a compiled styleable id of type int[] into the styleable name. + */ + public String resolveStyleable(int[] id) { + if (mStyleableValueToNameMap != null) { + mWrapper.set(id); + return mStyleableValueToNameMap.get(mWrapper); + } + + return null; + } + + /** + * Returns the integer id of a resource given its type and name. + * <p/>If the resource is of type {@link ResourceType#ID} and does not exist in the + * internal map, then new id values are dynamically generated (and stored so that queries + * with the same names will return the same value). + */ + public Integer getResourceId(ResourceType type, String name) { + Integer result = null; + if (mResourceValueMap != null) { + Map<String, Integer> map = mResourceValueMap.get(type); + if (map != null) { + result = map.get(name); + } + } + + if (result == null) { + synchronized (mDynamicIdMap) { + result = mDynamicIdMap.getId(type, name); + } + } + + return result; + } + + /** + * Resets the list of dynamic Ids. This list is used by + * {@link #getResourceId(String, String)} when the resource query is an ID that doesn't + * exist (for example for ID automatically generated in layout files that are not saved yet.) + * <p/>This method resets those dynamic ID and must be called whenever the actual list of IDs + * change. + */ + public void resetDynamicIds() { + synchronized (mDynamicIdMap) { + mDynamicIdMap.reset(DYNAMIC_ID_SEED_START); + } + } + + @Override + @NonNull + protected ResourceItem createResourceItem(@NonNull String name) { + return new ResourceItem(name); + } + + /** + * Sets compiled resource information. + * + * @param resIdValueToNameMap a map of compiled resource id to resource name. + * The map is acquired by the {@link ProjectResources} object. + * @param styleableValueMap a map of (int[], name) for the styleable information. The map is + * acquired by the {@link ProjectResources} object. + * @param resourceValueMap a map of (name, id) for resources of type {@link ResourceType#ID}. + * The list is acquired by the {@link ProjectResources} object. + */ + void setCompiledResources(Map<Integer, Pair<ResourceType, String>> resIdValueToNameMap, + Map<IntArrayWrapper, String> styleableValueMap, + Map<ResourceType, Map<String, Integer>> resourceValueMap) { + mResourceValueMap = resourceValueMap; + mResIdValueToNameMap = resIdValueToNameMap; + mStyleableValueToNameMap = styleableValueMap; + + resetDynamicIds(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java new file mode 100644 index 000000000..e407b6a78 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java @@ -0,0 +1,655 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.eclipse.adt.internal.resources.manager; + +import com.android.SdkConstants; +import com.android.ide.common.resources.FrameworkResources; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.resources.ResourceFolder; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ScanningContext; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IRawDeltaListener; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.FolderWrapper; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.QualifiedName; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * The ResourceManager tracks resources for all opened projects. + * <p/> + * It provide direct access to all the resources of a project as a {@link ProjectResources} + * object that allows accessing the resources through their file representation or as Android + * resources (similar to what is seen by an Android application). + * <p/> + * The ResourceManager automatically tracks file changes to update its internal representation + * of the resources so that they are always up to date. + * <p/> + * It also gives access to a monitor that is more resource oriented than the + * {@link GlobalProjectMonitor}. + * This monitor will let you track resource changes by giving you direct access to + * {@link ResourceFile}, or {@link ResourceFolder}. + * + * @see ProjectResources + */ +public final class ResourceManager { + public final static boolean DEBUG = false; + + private final static ResourceManager sThis = new ResourceManager(); + + /** + * Map associating project resource with project objects. + * <p/><b>All accesses must be inside a synchronized(mMap) block</b>, and do as a little as + * possible and <b>not call out to other classes</b>. + */ + private final Map<IProject, ProjectResources> mMap = + new HashMap<IProject, ProjectResources>(); + + /** + * Interface to be notified of resource changes. + * + * @see ResourceManager#addListener(IResourceListener) + * @see ResourceManager#removeListener(IResourceListener) + */ + public interface IResourceListener { + /** + * Notification for resource file change. + * @param project the project of the file. + * @param file the {@link ResourceFile} representing the file. + * @param eventType the type of event. See {@link IResourceDelta}. + */ + void fileChanged(IProject project, ResourceFile file, int eventType); + /** + * Notification for resource folder change. + * @param project the project of the file. + * @param folder the {@link ResourceFolder} representing the folder. + * @param eventType the type of event. See {@link IResourceDelta}. + */ + void folderChanged(IProject project, ResourceFolder folder, int eventType); + } + + private final ArrayList<IResourceListener> mListeners = new ArrayList<IResourceListener>(); + + /** + * Sets up the resource manager with the global project monitor. + * @param monitor The global project monitor + */ + public static void setup(GlobalProjectMonitor monitor) { + monitor.addProjectListener(sThis.mProjectListener); + monitor.addRawDeltaListener(sThis.mRawDeltaListener); + + CompiledResourcesMonitor.setupMonitor(monitor); + } + + /** + * Returns the singleton instance. + */ + public static ResourceManager getInstance() { + return sThis; + } + + /** + * Adds a new {@link IResourceListener} to be notified of resource changes. + * @param listener the listener to be added. + */ + public void addListener(IResourceListener listener) { + synchronized (mListeners) { + mListeners.add(listener); + } + } + + /** + * Removes an {@link IResourceListener}, so that it's not notified of resource changes anymore. + * @param listener the listener to be removed. + */ + public void removeListener(IResourceListener listener) { + synchronized (mListeners) { + mListeners.remove(listener); + } + } + + /** + * Returns the resources of a project. + * @param project The project + * @return a ProjectResources object + */ + public ProjectResources getProjectResources(IProject project) { + synchronized (mMap) { + ProjectResources resources = mMap.get(project); + + if (resources == null) { + resources = ProjectResources.create(project); + mMap.put(project, resources); + } + + return resources; + } + } + + /** + * Update the resource repository with a delta + * + * @param delta the resource changed delta to process. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + */ + public void processDelta(IResourceDelta delta, IdeScanningContext context) { + doProcessDelta(delta, context); + + // when a project is added to the workspace it is possible this is called before the + // repo is actually created so this will return null. + ResourceRepository repo = context.getRepository(); + if (repo != null) { + repo.postUpdateCleanUp(); + } + } + + /** + * Update the resource repository with a delta + * + * @param delta the resource changed delta to process. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + */ + private void doProcessDelta(IResourceDelta delta, IdeScanningContext context) { + // Skip over deltas that don't fit our mask + int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED; + int kind = delta.getKind(); + if ( (mask & kind) == 0) { + return; + } + + // Process this delta first as we need to make sure new folders are created before + // we process their content + IResource r = delta.getResource(); + int type = r.getType(); + + if (type == IResource.FILE) { + context.startScanning(r); + updateFile((IFile)r, delta.getMarkerDeltas(), kind, context); + context.finishScanning(r); + } else if (type == IResource.FOLDER) { + updateFolder((IFolder)r, kind, context); + } // We only care about files and folders. + // Project deltas are handled by our project listener + + // Now, process children recursively + IResourceDelta[] children = delta.getAffectedChildren(); + for (IResourceDelta child : children) { + processDelta(child, context); + } + } + + /** + * Update a resource folder that we know about + * @param folder the folder that was updated + * @param kind the delta type (added/removed/updated) + */ + private void updateFolder(IFolder folder, int kind, IdeScanningContext context) { + ProjectResources resources; + + final IProject project = folder.getProject(); + + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + return; + } + } catch (CoreException e) { + // can't get the project nature? return! + return; + } + + switch (kind) { + case IResourceDelta.ADDED: + // checks if the folder is under res. + IPath path = folder.getFullPath(); + + // the path will be project/res/<something> + if (path.segmentCount() == 3) { + if (isInResFolder(path)) { + // get the project and its resource object. + synchronized (mMap) { + resources = mMap.get(project); + + // if it doesn't exist, we create it. + if (resources == null) { + resources = ProjectResources.create(project); + mMap.put(project, resources); + } + } + + ResourceFolder newFolder = resources.processFolder( + new IFolderWrapper(folder)); + if (newFolder != null) { + notifyListenerOnFolderChange(project, newFolder, kind); + } + } + } + break; + case IResourceDelta.CHANGED: + // only call the listeners. + synchronized (mMap) { + resources = mMap.get(folder.getProject()); + } + if (resources != null) { + ResourceFolder resFolder = resources.getResourceFolder(folder); + if (resFolder != null) { + notifyListenerOnFolderChange(project, resFolder, kind); + } + } + break; + case IResourceDelta.REMOVED: + synchronized (mMap) { + resources = mMap.get(folder.getProject()); + } + if (resources != null) { + // lets get the folder type + ResourceFolderType type = ResourceFolderType.getFolderType( + folder.getName()); + + context.startScanning(folder); + ResourceFolder removedFolder = resources.removeFolder(type, + new IFolderWrapper(folder), context); + context.finishScanning(folder); + if (removedFolder != null) { + notifyListenerOnFolderChange(project, removedFolder, kind); + } + } + break; + } + } + + /** + * Called when a delta indicates that a file has changed. Depending on the + * file being changed, and the type of change (ADDED, REMOVED, CHANGED), the + * file change is processed to update the resource manager data. + * + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + */ + private void updateFile(IFile file, IMarkerDelta[] markerDeltas, int kind, + ScanningContext context) { + final IProject project = file.getProject(); + + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + return; + } + } catch (CoreException e) { + // can't get the project nature? return! + return; + } + + // get the project resources + ProjectResources resources; + synchronized (mMap) { + resources = mMap.get(project); + } + + if (resources == null) { + return; + } + + // checks if the file is under res/something or bin/res/something + IPath path = file.getFullPath(); + + if (path.segmentCount() == 4 || path.segmentCount() == 5) { + if (isInResFolder(path)) { + IContainer container = file.getParent(); + if (container instanceof IFolder) { + + ResourceFolder folder = resources.getResourceFolder( + (IFolder)container); + + // folder can be null as when the whole folder is deleted, the + // REMOVED event for the folder comes first. In this case, the + // folder will have taken care of things. + if (folder != null) { + ResourceFile resFile = folder.processFile( + new IFileWrapper(file), + ResourceHelper.getResourceDeltaKind(kind), context); + notifyListenerOnFileChange(project, resFile, kind); + } + } + } + } + } + + /** + * Implementation of the {@link IProjectListener} as an internal class so that the methods + * do not appear in the public API of {@link ResourceManager}. + */ + private final IProjectListener mProjectListener = new IProjectListener() { + @Override + public void projectClosed(IProject project) { + synchronized (mMap) { + mMap.remove(project); + } + } + + @Override + public void projectDeleted(IProject project) { + synchronized (mMap) { + mMap.remove(project); + } + } + + @Override + public void projectOpened(IProject project) { + createProject(project); + } + + @Override + public void projectOpenedWithWorkspace(IProject project) { + createProject(project); + } + + @Override + public void allProjectsOpenedWithWorkspace() { + // nothing to do. + } + + @Override + public void projectRenamed(IProject project, IPath from) { + // renamed project get a delete/open event too, so this can be ignored. + } + }; + + /** + * Implementation of {@link IRawDeltaListener} as an internal class so that the methods + * do not appear in the public API of {@link ResourceManager}. Delta processing can be + * accessed through the {@link ResourceManager#visitDelta(IResourceDelta delta)} method. + */ + private final IRawDeltaListener mRawDeltaListener = new IRawDeltaListener() { + @Override + public void visitDelta(IResourceDelta workspaceDelta) { + // If we're auto-building, then PreCompilerBuilder will pass us deltas and + // they will be processed as part of the build. + if (isAutoBuilding()) { + return; + } + + // When *not* auto building, we need to process the deltas immediately on save, + // even if the user is not building yet, such that for example resource ids + // are updated in the resource repositories so rendering etc. can work for + // those new ids. + + IResourceDelta[] projectDeltas = workspaceDelta.getAffectedChildren(); + for (IResourceDelta delta : projectDeltas) { + if (delta.getResource() instanceof IProject) { + IProject project = (IProject) delta.getResource(); + + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + continue; + } + } catch (CoreException e) { + // only happens if the project is closed or doesn't exist. + } + + IdeScanningContext context = + new IdeScanningContext(getProjectResources(project), project, true); + + processDelta(delta, context); + + Collection<IProject> projects = context.getAaptRequestedProjects(); + if (projects != null) { + for (IProject p : projects) { + markAaptRequested(p); + } + } + } else { + AdtPlugin.log(IStatus.WARNING, "Unexpected delta type: %1$s", + delta.getResource().toString()); + } + } + } + }; + + /** + * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists. + */ + public ResourceFolder getResourceFolder(IFile file) { + IContainer container = file.getParent(); + if (container.getType() == IResource.FOLDER) { + IFolder parent = (IFolder)container; + IProject project = file.getProject(); + + ProjectResources resources = getProjectResources(project); + if (resources != null) { + return resources.getResourceFolder(parent); + } + } + + return null; + } + + /** + * Returns the {@link ResourceFolder} for the given folder or <code>null</code> if none exists. + */ + public ResourceFolder getResourceFolder(IFolder folder) { + IProject project = folder.getProject(); + + ProjectResources resources = getProjectResources(project); + if (resources != null) { + return resources.getResourceFolder(folder); + } + + return null; + } + + /** + * Loads and returns the resources for a given {@link IAndroidTarget} + * @param androidTarget the target from which to load the framework resources + */ + public ResourceRepository loadFrameworkResources(IAndroidTarget androidTarget) { + String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES); + + FolderWrapper frameworkRes = new FolderWrapper(osResourcesPath); + if (frameworkRes.exists()) { + FrameworkResources resources = new FrameworkResources(frameworkRes); + + resources.loadResources(); + resources.loadPublicResources(AdtPlugin.getDefault()); + return resources; + } + + return null; + } + + /** + * Initial project parsing to gather resource info. + * @param project + */ + private void createProject(IProject project) { + if (project.isOpen()) { + synchronized (mMap) { + ProjectResources projectResources = mMap.get(project); + if (projectResources == null) { + projectResources = ProjectResources.create(project); + mMap.put(project, projectResources); + } + } + } + } + + + /** + * Returns true if the path is under /project/res/ + * @param path a workspace relative path + * @return true if the path is under /project res/ + */ + private boolean isInResFolder(IPath path) { + return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1)); + } + + private void notifyListenerOnFolderChange(IProject project, ResourceFolder folder, + int eventType) { + synchronized (mListeners) { + for (IResourceListener listener : mListeners) { + try { + listener.folderChanged(project, folder, eventType); + } catch (Throwable t) { + AdtPlugin.log(t, + "Failed to execute ResourceManager.IResouceListener.folderChanged()"); //$NON-NLS-1$ + } + } + } + } + + private void notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType) { + synchronized (mListeners) { + for (IResourceListener listener : mListeners) { + try { + listener.fileChanged(project, file, eventType); + } catch (Throwable t) { + AdtPlugin.log(t, + "Failed to execute ResourceManager.IResouceListener.fileChanged()"); //$NON-NLS-1$ + } + } + } + } + + /** + * Private constructor to enforce singleton design. + */ + private ResourceManager() { + } + + // debug only + @SuppressWarnings("unused") + private String getKindString(int kind) { + if (DEBUG) { + switch (kind) { + case IResourceDelta.ADDED: return "ADDED"; + case IResourceDelta.REMOVED: return "REMOVED"; + case IResourceDelta.CHANGED: return "CHANGED"; + } + } + + return Integer.toString(kind); + } + + /** + * Returns true if the Project > Build Automatically option is turned on + * (default). + * + * @return true if the Project > Build Automatically option is turned on + * (default). + */ + public static boolean isAutoBuilding() { + return ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding(); + } + + /** Qualified name for the per-project persistent property "needs aapt" */ + private final static QualifiedName NEED_AAPT = new QualifiedName(AdtPlugin.PLUGIN_ID, + "aapt");//$NON-NLS-1$ + + /** + * Mark the given project, and any projects which depend on it as a library + * project, as needing a full aapt build the next time the project is built. + * + * @param project the project to mark as needing aapt + */ + public static void markAaptRequested(IProject project) { + try { + String needsAapt = Boolean.TRUE.toString(); + project.setPersistentProperty(NEED_AAPT, needsAapt); + + ProjectState state = Sdk.getProjectState(project); + if (state.isLibrary()) { + // For library projects also mark the dependent projects as needing full aapt + for (ProjectState parent : state.getFullParentProjects()) { + IProject parentProject = parent.getProject(); + // Mark the project, but only if it's open. Resource#setPersistentProperty + // only works on open projects. + if (parentProject.isOpen()) { + parentProject.setPersistentProperty(NEED_AAPT, needsAapt); + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + /** + * Clear the "needs aapt" flag set by {@link #markAaptRequested(IProject)}. + * This is usually called when a project is built. Note that this will only + * clean the build flag on the given project, not on any downstream projects + * that depend on this project as a library project. + * + * @param project the project to clear from the needs aapt list + */ + public static void clearAaptRequest(IProject project) { + try { + project.setPersistentProperty(NEED_AAPT, null); + // Note that even if this project is a library project, we -don't- clear + // the aapt flags on the dependent projects since they may still depend + // on other dirty projects. When they are built, they will issue their + // own clear flag requests. + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + /** + * Returns whether the given project needs a full aapt build. + * + * @param project the project to check + * @return true if the project needs a full aapt run + */ + public static boolean isAaptRequested(IProject project) { + try { + String b = project.getPersistentProperty(NEED_AAPT); + return b != null && Boolean.valueOf(b); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return false; + } +} |