blob: e407b6a7844565ba74792838084673ca050ea1ad [file] [log] [blame]
/*
* 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;
}
}