diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java | 655 |
1 files changed, 655 insertions, 0 deletions
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; + } +} |