| /* |
| * 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; |
| } |
| } |