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