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
17package com.android.ide.eclipse.adt.internal.resources.manager;
18
19import com.android.SdkConstants;
20import com.android.ide.common.resources.FrameworkResources;
21import com.android.ide.common.resources.ResourceFile;
22import com.android.ide.common.resources.ResourceFolder;
23import com.android.ide.common.resources.ResourceRepository;
24import com.android.ide.common.resources.ScanningContext;
25import com.android.ide.eclipse.adt.AdtConstants;
26import com.android.ide.eclipse.adt.AdtPlugin;
27import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
28import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
29import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IRawDeltaListener;
30import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
31import com.android.ide.eclipse.adt.internal.sdk.Sdk;
32import com.android.ide.eclipse.adt.io.IFileWrapper;
33import com.android.ide.eclipse.adt.io.IFolderWrapper;
34import com.android.io.FolderWrapper;
35import com.android.resources.ResourceFolderType;
36import com.android.sdklib.IAndroidTarget;
37
38import org.eclipse.core.resources.IContainer;
39import org.eclipse.core.resources.IFile;
40import org.eclipse.core.resources.IFolder;
41import org.eclipse.core.resources.IMarkerDelta;
42import org.eclipse.core.resources.IProject;
43import org.eclipse.core.resources.IResource;
44import org.eclipse.core.resources.IResourceDelta;
45import org.eclipse.core.resources.IResourceDeltaVisitor;
46import org.eclipse.core.resources.ResourcesPlugin;
47import org.eclipse.core.runtime.CoreException;
48import org.eclipse.core.runtime.IPath;
49import org.eclipse.core.runtime.IStatus;
50import org.eclipse.core.runtime.QualifiedName;
51
52import java.io.IOException;
53import java.util.ArrayList;
54import java.util.Collection;
55import java.util.HashMap;
56import 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 */
75public 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         */
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         */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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, true);
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     */
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     */
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     */
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     */
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, true);
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     */
571    private boolean isInResFolder(IPath path) {
572        return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
573    }
574
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
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     */
605    private ResourceManager() {
606    }
607
608    // debug only
609    @SuppressWarnings("unused")
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     */
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     */
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     */
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     */
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