Home | History | Annotate | Download | only in manager
      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          */
    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          */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    525     private boolean isInResFolder(IPath path) {
    526         return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
    527     }
    528 
    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 
    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      */
    559     private ResourceManager() {
    560     }
    561 
    562     // debug only
    563     @SuppressWarnings("unused")
    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      */
    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      */
    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      */
    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      */
    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