• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.resources.manager;
18 
19 import com.android.ide.common.resources.FrameworkResources;
20 import com.android.ide.common.resources.ResourceFile;
21 import com.android.ide.common.resources.ResourceFolder;
22 import com.android.ide.common.resources.ResourceRepository;
23 import com.android.ide.common.resources.ScanningContext;
24 import com.android.ide.eclipse.adt.AdtConstants;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
27 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IRawDeltaListener;
29 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
30 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
31 import com.android.ide.eclipse.adt.io.IFileWrapper;
32 import com.android.ide.eclipse.adt.io.IFolderWrapper;
33 import com.android.io.FolderWrapper;
34 import com.android.resources.ResourceFolderType;
35 import com.android.sdklib.IAndroidTarget;
36 import com.android.sdklib.SdkConstants;
37 
38 import org.eclipse.core.resources.IContainer;
39 import org.eclipse.core.resources.IFile;
40 import org.eclipse.core.resources.IFolder;
41 import org.eclipse.core.resources.IMarkerDelta;
42 import org.eclipse.core.resources.IProject;
43 import org.eclipse.core.resources.IResource;
44 import org.eclipse.core.resources.IResourceDelta;
45 import org.eclipse.core.resources.IResourceDeltaVisitor;
46 import org.eclipse.core.resources.ResourcesPlugin;
47 import org.eclipse.core.runtime.CoreException;
48 import org.eclipse.core.runtime.IPath;
49 import org.eclipse.core.runtime.IStatus;
50 import org.eclipse.core.runtime.QualifiedName;
51 
52 import java.io.IOException;
53 import java.util.ArrayList;
54 import java.util.Collection;
55 import java.util.HashMap;
56 import java.util.Map;
57 
58 /**
59  * The ResourceManager tracks resources for all opened projects.
60  * <p/>
61  * It provide direct access to all the resources of a project as a {@link ProjectResources}
62  * object that allows accessing the resources through their file representation or as Android
63  * resources (similar to what is seen by an Android application).
64  * <p/>
65  * The ResourceManager automatically tracks file changes to update its internal representation
66  * of the resources so that they are always up to date.
67  * <p/>
68  * It also gives access to a monitor that is more resource oriented than the
69  * {@link GlobalProjectMonitor}.
70  * This monitor will let you track resource changes by giving you direct access to
71  * {@link ResourceFile}, or {@link ResourceFolder}.
72  *
73  * @see ProjectResources
74  */
75 public final class ResourceManager {
76     public final static boolean DEBUG = false;
77 
78     private final static ResourceManager sThis = new ResourceManager();
79 
80     /**
81      * Map associating project resource with project objects.
82      * <p/><b>All accesses must be inside a synchronized(mMap) block</b>, and do as a little as
83      * possible and <b>not call out to other classes</b>.
84      */
85     private final Map<IProject, ProjectResources> mMap =
86         new HashMap<IProject, ProjectResources>();
87 
88     /**
89      * Interface to be notified of resource changes.
90      *
91      * @see ResourceManager#addListener(IResourceListener)
92      * @see ResourceManager#removeListener(IResourceListener)
93      */
94     public interface IResourceListener {
95         /**
96          * Notification for resource file change.
97          * @param project the project of the file.
98          * @param file the {@link ResourceFile} representing the file.
99          * @param eventType the type of event. See {@link IResourceDelta}.
100          */
fileChanged(IProject project, ResourceFile file, int eventType)101         void fileChanged(IProject project, ResourceFile file, int eventType);
102         /**
103          * Notification for resource folder change.
104          * @param project the project of the file.
105          * @param folder the {@link ResourceFolder} representing the folder.
106          * @param eventType the type of event. See {@link IResourceDelta}.
107          */
folderChanged(IProject project, ResourceFolder folder, int eventType)108         void folderChanged(IProject project, ResourceFolder folder, int eventType);
109     }
110 
111     private final ArrayList<IResourceListener> mListeners = new ArrayList<IResourceListener>();
112 
113     /**
114      * Sets up the resource manager with the global project monitor.
115      * @param monitor The global project monitor
116      */
setup(GlobalProjectMonitor monitor)117     public static void setup(GlobalProjectMonitor monitor) {
118         monitor.addProjectListener(sThis.mProjectListener);
119         monitor.addRawDeltaListener(sThis.mRawDeltaListener);
120 
121         CompiledResourcesMonitor.setupMonitor(monitor);
122     }
123 
124     /**
125      * Returns the singleton instance.
126      */
getInstance()127     public static ResourceManager getInstance() {
128         return sThis;
129     }
130 
131     /**
132      * Adds a new {@link IResourceListener} to be notified of resource changes.
133      * @param listener the listener to be added.
134      */
addListener(IResourceListener listener)135     public void addListener(IResourceListener listener) {
136         synchronized (mListeners) {
137             mListeners.add(listener);
138         }
139     }
140 
141     /**
142      * Removes an {@link IResourceListener}, so that it's not notified of resource changes anymore.
143      * @param listener the listener to be removed.
144      */
removeListener(IResourceListener listener)145     public void removeListener(IResourceListener listener) {
146         synchronized (mListeners) {
147             mListeners.remove(listener);
148         }
149     }
150 
151     /**
152      * Returns the resources of a project.
153      * @param project The project
154      * @return a ProjectResources object or null if none was found.
155      */
getProjectResources(IProject project)156     public ProjectResources getProjectResources(IProject project) {
157         synchronized (mMap) {
158             ProjectResources resources = mMap.get(project);
159 
160             if (resources == null) {
161                 resources = new ProjectResources(project);
162                 mMap.put(project, resources);
163             }
164 
165             return resources;
166         }
167     }
168 
169     /**
170      * Update the resource repository with a delta
171      *
172      * @param delta the resource changed delta to process.
173      * @param context a context object with state for the current update, such
174      *            as a place to stash errors encountered
175      */
processDelta(IResourceDelta delta, IdeScanningContext context)176     public void processDelta(IResourceDelta delta, IdeScanningContext context) {
177         doProcessDelta(delta, context);
178 
179         // when a project is added to the workspace it is possible this is called before the
180         // repo is actually created so this will return null.
181         ResourceRepository repo = context.getRepository();
182         if (repo != null) {
183             repo.postUpdateCleanUp();
184         }
185     }
186 
187     /**
188      * Update the resource repository with a delta
189      *
190      * @param delta the resource changed delta to process.
191      * @param context a context object with state for the current update, such
192      *            as a place to stash errors encountered
193      */
doProcessDelta(IResourceDelta delta, IdeScanningContext context)194     private void doProcessDelta(IResourceDelta delta, IdeScanningContext context) {
195         // Skip over deltas that don't fit our mask
196         int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED;
197         int kind = delta.getKind();
198         if ( (mask & kind) == 0) {
199             return;
200         }
201 
202         // Process this delta first as we need to make sure new folders are created before
203         // we process their content
204         IResource r = delta.getResource();
205         int type = r.getType();
206 
207         if (type == IResource.FILE) {
208             context.startScanning(r);
209             updateFile((IFile)r, delta.getMarkerDeltas(), kind, context);
210             context.finishScanning(r);
211         } else if (type == IResource.FOLDER) {
212             updateFolder((IFolder)r, kind, context);
213         } // We only care about files and folders.
214           // Project deltas are handled by our project listener
215 
216         // Now, process children recursively
217         IResourceDelta[] children = delta.getAffectedChildren();
218         for (IResourceDelta child : children)  {
219             processDelta(child, context);
220         }
221     }
222 
223     /**
224      * Update a resource folder that we know about
225      * @param folder the folder that was updated
226      * @param kind the delta type (added/removed/updated)
227      */
updateFolder(IFolder folder, int kind, IdeScanningContext context)228     private void updateFolder(IFolder folder, int kind, IdeScanningContext context) {
229         ProjectResources resources;
230 
231         final IProject project = folder.getProject();
232 
233         try {
234             if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
235                 return;
236             }
237         } catch (CoreException e) {
238             // can't get the project nature? return!
239             return;
240         }
241 
242         switch (kind) {
243             case IResourceDelta.ADDED:
244                 // checks if the folder is under res.
245                 IPath path = folder.getFullPath();
246 
247                 // the path will be project/res/<something>
248                 if (path.segmentCount() == 3) {
249                     if (isInResFolder(path)) {
250                         // get the project and its resource object.
251                         synchronized (mMap) {
252                             resources = mMap.get(project);
253 
254                             // if it doesn't exist, we create it.
255                             if (resources == null) {
256                                 resources = new ProjectResources(project);
257                                 mMap.put(project, resources);
258                             }
259                         }
260 
261                         ResourceFolder newFolder = resources.processFolder(
262                                 new IFolderWrapper(folder));
263                         if (newFolder != null) {
264                             notifyListenerOnFolderChange(project, newFolder, kind);
265                         }
266                     }
267                 }
268                 break;
269             case IResourceDelta.CHANGED:
270                 // only call the listeners.
271                 synchronized (mMap) {
272                     resources = mMap.get(folder.getProject());
273                 }
274                 if (resources != null) {
275                     ResourceFolder resFolder = resources.getResourceFolder(folder);
276                     if (resFolder != null) {
277                         notifyListenerOnFolderChange(project, resFolder, kind);
278                     }
279                 }
280                 break;
281             case IResourceDelta.REMOVED:
282                 synchronized (mMap) {
283                     resources = mMap.get(folder.getProject());
284                 }
285                 if (resources != null) {
286                     // lets get the folder type
287                     ResourceFolderType type = ResourceFolderType.getFolderType(
288                             folder.getName());
289 
290                     context.startScanning(folder);
291                     ResourceFolder removedFolder = resources.removeFolder(type,
292                             new IFolderWrapper(folder), context);
293                     context.finishScanning(folder);
294                     if (removedFolder != null) {
295                         notifyListenerOnFolderChange(project, removedFolder, kind);
296                     }
297                 }
298                 break;
299         }
300     }
301 
302     /**
303      * Called when a delta indicates that a file has changed. Depending on the
304      * file being changed, and the type of change (ADDED, REMOVED, CHANGED), the
305      * file change is processed to update the resource manager data.
306      *
307      * @param file The file that changed.
308      * @param markerDeltas The marker deltas for the file.
309      * @param kind The change kind. This is equivalent to
310      *            {@link IResourceDelta#accept(IResourceDeltaVisitor)}
311      * @param context a context object with state for the current update, such
312      *            as a place to stash errors encountered
313      */
updateFile(IFile file, IMarkerDelta[] markerDeltas, int kind, ScanningContext context)314     private void updateFile(IFile file, IMarkerDelta[] markerDeltas, int kind,
315             ScanningContext context) {
316         final IProject project = file.getProject();
317 
318         try {
319             if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
320                 return;
321             }
322         } catch (CoreException e) {
323             // can't get the project nature? return!
324             return;
325         }
326 
327         // get the project resources
328         ProjectResources resources;
329         synchronized (mMap) {
330             resources = mMap.get(project);
331         }
332 
333         if (resources == null) {
334             return;
335         }
336 
337         // checks if the file is under res/something or bin/res/something
338         IPath path = file.getFullPath();
339 
340         if (path.segmentCount() == 4 || path.segmentCount() == 5) {
341             if (isInResFolder(path)) {
342                 IContainer container = file.getParent();
343                 if (container instanceof IFolder) {
344 
345                     ResourceFolder folder = resources.getResourceFolder(
346                             (IFolder)container);
347 
348                     // folder can be null as when the whole folder is deleted, the
349                     // REMOVED event for the folder comes first. In this case, the
350                     // folder will have taken care of things.
351                     if (folder != null) {
352                         ResourceFile resFile = folder.processFile(
353                                 new IFileWrapper(file),
354                                 ResourceHelper.getResourceDeltaKind(kind), context);
355                         notifyListenerOnFileChange(project, resFile, kind);
356                     }
357                 }
358             }
359         }
360     }
361 
362     /**
363      * Implementation of the {@link IProjectListener} as an internal class so that the methods
364      * do not appear in the public API of {@link ResourceManager}.
365      */
366     private final IProjectListener mProjectListener = new IProjectListener() {
367         @Override
368         public void projectClosed(IProject project) {
369             synchronized (mMap) {
370                 mMap.remove(project);
371             }
372         }
373 
374         @Override
375         public void projectDeleted(IProject project) {
376             synchronized (mMap) {
377                 mMap.remove(project);
378             }
379         }
380 
381         @Override
382         public void projectOpened(IProject project) {
383             createProject(project);
384         }
385 
386         @Override
387         public void projectOpenedWithWorkspace(IProject project) {
388             createProject(project);
389         }
390 
391         @Override
392         public void allProjectsOpenedWithWorkspace() {
393             // nothing to do.
394         }
395 
396         @Override
397         public void projectRenamed(IProject project, IPath from) {
398             // renamed project get a delete/open event too, so this can be ignored.
399         }
400     };
401 
402     /**
403      * Implementation of {@link IRawDeltaListener} as an internal class so that the methods
404      * do not appear in the public API of {@link ResourceManager}. Delta processing can be
405      * accessed through the {@link ResourceManager#visitDelta(IResourceDelta delta)} method.
406      */
407     private final IRawDeltaListener mRawDeltaListener = new IRawDeltaListener() {
408         @Override
409         public void visitDelta(IResourceDelta workspaceDelta) {
410             // If we're auto-building, then PreCompilerBuilder will pass us deltas and
411             // they will be processed as part of the build.
412             if (isAutoBuilding()) {
413                 return;
414             }
415 
416             // When *not* auto building, we need to process the deltas immediately on save,
417             // even if the user is not building yet, such that for example resource ids
418             // are updated in the resource repositories so rendering etc. can work for
419             // those new ids.
420 
421             IResourceDelta[] projectDeltas = workspaceDelta.getAffectedChildren();
422             for (IResourceDelta delta : projectDeltas) {
423                 if (delta.getResource() instanceof IProject) {
424                     IProject project = (IProject) delta.getResource();
425                     IdeScanningContext context =
426                             new IdeScanningContext(getProjectResources(project), project);
427 
428                     processDelta(delta, context);
429 
430                     Collection<IProject> projects = context.getAaptRequestedProjects();
431                     if (projects != null) {
432                         for (IProject p : projects) {
433                             markAaptRequested(p);
434                         }
435                     }
436                 } else {
437                     AdtPlugin.log(IStatus.WARNING, "Unexpected delta type: %1$s",
438                             delta.getResource().toString());
439                 }
440             }
441         }
442     };
443 
444     /**
445      * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists.
446      */
getResourceFolder(IFile file)447     public ResourceFolder getResourceFolder(IFile file) {
448         IContainer container = file.getParent();
449         if (container.getType() == IResource.FOLDER) {
450             IFolder parent = (IFolder)container;
451             IProject project = file.getProject();
452 
453             ProjectResources resources = getProjectResources(project);
454             if (resources != null) {
455                 return resources.getResourceFolder(parent);
456             }
457         }
458 
459         return null;
460     }
461 
462     /**
463      * Returns the {@link ResourceFolder} for the given folder or <code>null</code> if none exists.
464      */
getResourceFolder(IFolder folder)465     public ResourceFolder getResourceFolder(IFolder folder) {
466         IProject project = folder.getProject();
467 
468         ProjectResources resources = getProjectResources(project);
469         if (resources != null) {
470             return resources.getResourceFolder(folder);
471         }
472 
473         return null;
474     }
475 
476     /**
477      * Loads and returns the resources for a given {@link IAndroidTarget}
478      * @param androidTarget the target from which to load the framework resources
479      */
loadFrameworkResources(IAndroidTarget androidTarget)480     public ResourceRepository loadFrameworkResources(IAndroidTarget androidTarget) {
481         String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES);
482 
483         FolderWrapper frameworkRes = new FolderWrapper(osResourcesPath);
484         if (frameworkRes.exists()) {
485             FrameworkResources resources = new FrameworkResources();
486 
487             try {
488                 resources.loadResources(frameworkRes);
489                 resources.loadPublicResources(frameworkRes, AdtPlugin.getDefault());
490                 return resources;
491             } catch (IOException e) {
492                 // since we test that folders are folders, and files are files, this shouldn't
493                 // happen. We can ignore it.
494             }
495         }
496 
497         return null;
498     }
499 
500     /**
501      * Initial project parsing to gather resource info.
502      * @param project
503      */
createProject(IProject project)504     private void createProject(IProject project) {
505         if (project.isOpen()) {
506             try {
507                 if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
508                     return;
509                 }
510             } catch (CoreException e1) {
511                 // can't check the nature of the project? ignore it.
512                 return;
513             }
514 
515             IFolder resourceFolder = project.getFolder(SdkConstants.FD_RESOURCES);
516 
517             ProjectResources projectResources;
518             synchronized (mMap) {
519                 projectResources = mMap.get(project);
520                 if (projectResources == null) {
521                     projectResources = new ProjectResources(project);
522                     mMap.put(project, projectResources);
523                 }
524             }
525             IdeScanningContext context = new IdeScanningContext(projectResources, project);
526 
527             if (resourceFolder != null && resourceFolder.exists()) {
528                 try {
529                     IResource[] resources = resourceFolder.members();
530 
531                     for (IResource res : resources) {
532                         if (res.getType() == IResource.FOLDER) {
533                             IFolder folder = (IFolder)res;
534                             ResourceFolder resFolder = projectResources.processFolder(
535                                     new IFolderWrapper(folder));
536 
537                             if (resFolder != null) {
538                                 // now we process the content of the folder
539                                 IResource[] files = folder.members();
540 
541                                 for (IResource fileRes : files) {
542                                     if (fileRes.getType() == IResource.FILE) {
543                                         IFile file = (IFile)fileRes;
544 
545                                         context.startScanning(file);
546 
547                                         resFolder.processFile(new IFileWrapper(file),
548                                                 ResourceHelper.getResourceDeltaKind(
549                                                         IResourceDelta.ADDED), context);
550 
551                                         context.finishScanning(file);
552                                     }
553                                 }
554                             }
555                         }
556                     }
557                 } catch (CoreException e) {
558                     // This happens if the project is closed or if the folder doesn't exist.
559                     // Since we already test for that, we can ignore this exception.
560                 }
561             }
562         }
563     }
564 
565 
566     /**
567      * Returns true if the path is under /project/res/
568      * @param path a workspace relative path
569      * @return true if the path is under /project res/
570      */
isInResFolder(IPath path)571     private boolean isInResFolder(IPath path) {
572         return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
573     }
574 
notifyListenerOnFolderChange(IProject project, ResourceFolder folder, int eventType)575     private void notifyListenerOnFolderChange(IProject project, ResourceFolder folder,
576             int eventType) {
577         synchronized (mListeners) {
578             for (IResourceListener listener : mListeners) {
579                 try {
580                     listener.folderChanged(project, folder, eventType);
581                 } catch (Throwable t) {
582                     AdtPlugin.log(t,
583                             "Failed to execute ResourceManager.IResouceListener.folderChanged()"); //$NON-NLS-1$
584                 }
585             }
586         }
587     }
588 
notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType)589     private void notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType) {
590         synchronized (mListeners) {
591             for (IResourceListener listener : mListeners) {
592                 try {
593                     listener.fileChanged(project, file, eventType);
594                 } catch (Throwable t) {
595                     AdtPlugin.log(t,
596                             "Failed to execute ResourceManager.IResouceListener.fileChanged()"); //$NON-NLS-1$
597                 }
598             }
599         }
600     }
601 
602     /**
603      * Private constructor to enforce singleton design.
604      */
ResourceManager()605     private ResourceManager() {
606     }
607 
608     // debug only
609     @SuppressWarnings("unused")
getKindString(int kind)610     private String getKindString(int kind) {
611         if (DEBUG) {
612             switch (kind) {
613                 case IResourceDelta.ADDED: return "ADDED";
614                 case IResourceDelta.REMOVED: return "REMOVED";
615                 case IResourceDelta.CHANGED: return "CHANGED";
616             }
617         }
618 
619         return Integer.toString(kind);
620     }
621 
622     /**
623      * Returns true if the Project > Build Automatically option is turned on
624      * (default).
625      *
626      * @return true if the Project > Build Automatically option is turned on
627      *         (default).
628      */
isAutoBuilding()629     public static boolean isAutoBuilding() {
630         return ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
631     }
632 
633     /** Qualified name for the per-project persistent property "needs aapt" */
634     private final static QualifiedName NEED_AAPT = new QualifiedName(AdtPlugin.PLUGIN_ID,
635             "aapt");//$NON-NLS-1$
636 
637     /**
638      * Mark the given project, and any projects which depend on it as a library
639      * project, as needing a full aapt build the next time the project is built.
640      *
641      * @param project the project to mark as needing aapt
642      */
markAaptRequested(IProject project)643     public static void markAaptRequested(IProject project) {
644         try {
645             String needsAapt = Boolean.TRUE.toString();
646             project.setPersistentProperty(NEED_AAPT, needsAapt);
647 
648             ProjectState state = Sdk.getProjectState(project);
649             if (state.isLibrary()) {
650                 // For library projects also mark the dependent projects as needing full aapt
651                 for (ProjectState parent : state.getFullParentProjects()) {
652                     IProject parentProject = parent.getProject();
653                     // Mark the project, but only if it's open. Resource#setPersistentProperty
654                     // only works on open projects.
655                     if (parentProject.isOpen()) {
656                         parentProject.setPersistentProperty(NEED_AAPT, needsAapt);
657                     }
658                 }
659             }
660         } catch (CoreException e) {
661             AdtPlugin.log(e,  null);
662         }
663     }
664 
665     /**
666      * Clear the "needs aapt" flag set by {@link #markAaptRequested(IProject)}.
667      * This is usually called when a project is built. Note that this will only
668      * clean the build flag on the given project, not on any downstream projects
669      * that depend on this project as a library project.
670      *
671      * @param project the project to clear from the needs aapt list
672      */
clearAaptRequest(IProject project)673     public static void clearAaptRequest(IProject project) {
674         try {
675             project.setPersistentProperty(NEED_AAPT, null);
676             // Note that even if this project is a library project, we -don't- clear
677             // the aapt flags on the dependent projects since they may still depend
678             // on other dirty projects. When they are built, they will issue their
679             // own clear flag requests.
680         } catch (CoreException e) {
681             AdtPlugin.log(e,  null);
682         }
683     }
684 
685     /**
686      * Returns whether the given project needs a full aapt build.
687      *
688      * @param project the project to check
689      * @return true if the project needs a full aapt run
690      */
isAaptRequested(IProject project)691     public static boolean isAaptRequested(IProject project) {
692         try {
693             String b = project.getPersistentProperty(NEED_AAPT);
694             return b != null && Boolean.valueOf(b);
695         } catch (CoreException e) {
696             AdtPlugin.log(e,  null);
697         }
698 
699         return false;
700     }
701 }
702