Home | History | Annotate | Download | only in layout
      1 /*
      2  * Copyright (C) 2008 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.editors.layout;
     18 
     19 import com.android.ide.common.resources.ResourceFile;
     20 import com.android.ide.common.resources.ResourceFolder;
     21 import com.android.ide.eclipse.adt.AdtConstants;
     22 import com.android.ide.eclipse.adt.AdtPlugin;
     23 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
     24 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
     25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
     26 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     27 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
     28 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     29 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     30 import com.android.resources.ResourceType;
     31 import com.android.sdklib.SdkConstants;
     32 
     33 import org.eclipse.core.resources.IFile;
     34 import org.eclipse.core.resources.IMarkerDelta;
     35 import org.eclipse.core.resources.IProject;
     36 import org.eclipse.core.resources.IResourceDelta;
     37 import org.eclipse.core.runtime.CoreException;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Collection;
     41 import java.util.HashMap;
     42 import java.util.Iterator;
     43 import java.util.List;
     44 import java.util.Map;
     45 import java.util.Map.Entry;
     46 import java.util.Set;
     47 
     48 /**
     49  * Monitor for file changes that could trigger a layout redraw, or a UI update
     50  */
     51 public final class LayoutReloadMonitor {
     52 
     53     // singleton, enforced by private constructor.
     54     private final static LayoutReloadMonitor sThis = new LayoutReloadMonitor();
     55 
     56     /**
     57      * Map of listeners by IProject.
     58      */
     59     private final Map<IProject, List<ILayoutReloadListener>> mListenerMap =
     60         new HashMap<IProject, List<ILayoutReloadListener>>();
     61 
     62     public final static class ChangeFlags {
     63         public boolean code = false;
     64         /** any non-layout resource changes */
     65         public boolean resources = false;
     66         public boolean rClass = false;
     67         public boolean localeList = false;
     68         public boolean manifest = false;
     69 
     70         boolean isAllTrue() {
     71             return code && resources && rClass && localeList && manifest;
     72         }
     73     }
     74 
     75     /**
     76      * List of projects having received a resource change.
     77      */
     78     private final Map<IProject, ChangeFlags> mProjectFlags = new HashMap<IProject, ChangeFlags>();
     79 
     80     /**
     81      * Classes which implement this interface provide a method to respond to resource changes
     82      * triggering a layout redraw
     83      */
     84     public interface ILayoutReloadListener {
     85         /**
     86          * Sent when the layout needs to be redrawn
     87          *
     88          * @param flags a {@link ChangeFlags} object indicating what type of resource changed.
     89          * @param libraryModified <code>true</code> if the changeFlags are not for the project
     90          * associated with the listener, but instead correspond to a library.
     91          */
     92         void reloadLayout(ChangeFlags flags, boolean libraryModified);
     93     }
     94 
     95     /**
     96      * Returns the single instance of {@link LayoutReloadMonitor}.
     97      */
     98     public static LayoutReloadMonitor getMonitor() {
     99         return sThis;
    100     }
    101 
    102     private LayoutReloadMonitor() {
    103         // listen to resource changes. Used for non-layout resource (trigger a redraw), or
    104         // any resource folder (trigger a locale list refresh)
    105         ResourceManager.getInstance().addListener(mResourceListener);
    106 
    107         // also listen for .class file changed in case the layout has custom view classes.
    108         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    109         monitor.addFileListener(mFileListener,
    110                 IResourceDelta.ADDED | IResourceDelta.CHANGED | IResourceDelta.REMOVED);
    111 
    112         monitor.addResourceEventListener(mResourceEventListener);
    113     }
    114 
    115     /**
    116      * Adds a listener for a given {@link IProject}.
    117      * @param project
    118      * @param listener
    119      */
    120     public void addListener(IProject project, ILayoutReloadListener listener) {
    121         synchronized (mListenerMap) {
    122             List<ILayoutReloadListener> list = mListenerMap.get(project);
    123             if (list == null) {
    124                 list = new ArrayList<ILayoutReloadListener>();
    125                 mListenerMap.put(project, list);
    126             }
    127 
    128             list.add(listener);
    129         }
    130     }
    131 
    132     /**
    133      * Removes a listener for a given {@link IProject}.
    134      */
    135     public void removeListener(IProject project, ILayoutReloadListener listener) {
    136         synchronized (mListenerMap) {
    137             List<ILayoutReloadListener> list = mListenerMap.get(project);
    138             if (list != null) {
    139                 list.remove(listener);
    140             }
    141         }
    142     }
    143 
    144     /**
    145      * Removes a listener, no matter which {@link IProject} it was associated with.
    146      */
    147     public void removeListener(ILayoutReloadListener listener) {
    148         synchronized (mListenerMap) {
    149 
    150             for (List<ILayoutReloadListener> list : mListenerMap.values()) {
    151                 Iterator<ILayoutReloadListener> it = list.iterator();
    152                 while (it.hasNext()) {
    153                     ILayoutReloadListener i = it.next();
    154                     if (i == listener) {
    155                         it.remove();
    156                     }
    157                 }
    158             }
    159         }
    160     }
    161 
    162     /**
    163      * Implementation of the {@link IFileListener} as an internal class so that the methods
    164      * do not appear in the public API of {@link LayoutReloadMonitor}.
    165      *
    166      * This is only to detect code and manifest change. Resource changes (located in res/)
    167      * is done through {@link #mResourceListener}.
    168      */
    169     private IFileListener mFileListener = new IFileListener() {
    170         /*
    171          * Callback for IFileListener. Called when a file changed.
    172          * This records the changes for each project, but does not notify listeners.
    173          */
    174         @Override
    175         public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
    176             // get the file's project
    177             IProject project = file.getProject();
    178 
    179             boolean hasAndroidNature = false;
    180             try {
    181                 hasAndroidNature = project.hasNature(AdtConstants.NATURE_DEFAULT);
    182             } catch (CoreException e) {
    183                 // do nothing if the nature cannot be queried.
    184                 return;
    185             }
    186 
    187             if (hasAndroidNature) {
    188                 // project is an Android project, it's the one being affected
    189                 // directly by its own file change.
    190                 processFileChanged(file, project);
    191             } else {
    192                 // check the projects depending on it, if they are Android project, update them.
    193                 IProject[] referencingProjects = project.getReferencingProjects();
    194 
    195                 for (IProject p : referencingProjects) {
    196                     try {
    197                         hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT);
    198                     } catch (CoreException e) {
    199                         // do nothing if the nature cannot be queried.
    200                         continue;
    201                     }
    202 
    203                     if (hasAndroidNature) {
    204                         // the changed project is a dependency on an Android project,
    205                         // update the main project.
    206                         processFileChanged(file, p);
    207                     }
    208                 }
    209             }
    210         }
    211 
    212         /**
    213          * Processes a file change for a given project which may or may not be the file's project.
    214          * @param file the changed file
    215          * @param project the project impacted by the file change.
    216          */
    217         private void processFileChanged(IFile file, IProject project) {
    218             // if this project has already been marked as modified, we do nothing.
    219             ChangeFlags changeFlags = mProjectFlags.get(project);
    220             if (changeFlags != null && changeFlags.isAllTrue()) {
    221                 return;
    222             }
    223 
    224             // here we only care about code change (so change for .class files).
    225             // Resource changes is handled by the IResourceListener.
    226             if (AdtConstants.EXT_CLASS.equals(file.getFileExtension())) {
    227                 if (file.getName().matches("R[\\$\\.](.*)")) {
    228                     // this is a R change!
    229                     if (changeFlags == null) {
    230                         changeFlags = new ChangeFlags();
    231                         mProjectFlags.put(project, changeFlags);
    232                     }
    233 
    234                     changeFlags.rClass = true;
    235                 } else {
    236                     // this is a code change!
    237                     if (changeFlags == null) {
    238                         changeFlags = new ChangeFlags();
    239                         mProjectFlags.put(project, changeFlags);
    240                     }
    241 
    242                     changeFlags.code = true;
    243                 }
    244             } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()) &&
    245                     file.getParent().equals(project)) {
    246                 // this is a manifest change!
    247                 if (changeFlags == null) {
    248                     changeFlags = new ChangeFlags();
    249                     mProjectFlags.put(project, changeFlags);
    250                 }
    251 
    252                 changeFlags.manifest = true;
    253             }
    254         }
    255     };
    256 
    257     /**
    258      * Implementation of the {@link IResourceEventListener} as an internal class so that the methods
    259      * do not appear in the public API of {@link LayoutReloadMonitor}.
    260      */
    261     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
    262         /*
    263          * Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a
    264          * resource change event. This is called once, while fileChanged can be
    265          * called several times.
    266          *
    267          */
    268         @Override
    269         public void resourceChangeEventStart() {
    270             // nothing to be done here, it all happens in the resourceChangeEventEnd
    271         }
    272 
    273         /*
    274          * Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource
    275          * change event. This is where we notify the listeners.
    276          */
    277         @Override
    278         public void resourceChangeEventEnd() {
    279             // for each IProject that was changed, we notify all the listeners.
    280             for (Entry<IProject, ChangeFlags> entry : mProjectFlags.entrySet()) {
    281                 IProject project = entry.getKey();
    282 
    283                 // notify the project itself.
    284                 notifyForProject(project, entry.getValue(), false);
    285 
    286                 // check if the project is a library, and if it is search for what other
    287                 // project depends on this one (directly or not)
    288                 ProjectState state = Sdk.getProjectState(project);
    289                 if (state != null && state.isLibrary()) {
    290                     Set<ProjectState> mainProjects = Sdk.getMainProjectsFor(project);
    291                     for (ProjectState mainProject : mainProjects) {
    292                         // always give the changeflag of the modified project.
    293                         notifyForProject(mainProject.getProject(), entry.getValue(), true);
    294                     }
    295                 }
    296             }
    297 
    298             // empty the list.
    299             mProjectFlags.clear();
    300         }
    301 
    302         /**
    303          * Notifies the listeners for a given project.
    304          * @param project the project for which the listeners must be notified
    305          * @param flags the change flags to pass to the listener
    306          * @param libraryChanged a flag indicating if the change flags are for the give project,
    307          * or if they are for a library dependency.
    308          */
    309         private void notifyForProject(IProject project, ChangeFlags flags,
    310                 boolean libraryChanged) {
    311             synchronized (mListenerMap) {
    312                 List<ILayoutReloadListener> listeners = mListenerMap.get(project);
    313 
    314                 if (listeners != null) {
    315                     for (ILayoutReloadListener listener : listeners) {
    316                         try {
    317                             listener.reloadLayout(flags, libraryChanged);
    318                         } catch (Throwable t) {
    319                             AdtPlugin.log(t, "Failed to call ILayoutReloadListener.reloadLayout");
    320                         }
    321                     }
    322                 }
    323             }
    324         }
    325     };
    326 
    327     /**
    328      * Implementation of the {@link IResourceListener} as an internal class so that the methods
    329      * do not appear in the public API of {@link LayoutReloadMonitor}.
    330      */
    331     private IResourceListener mResourceListener = new IResourceListener() {
    332 
    333         @Override
    334         public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
    335             // if this project has already been marked as modified, we do nothing.
    336             ChangeFlags changeFlags = mProjectFlags.get(project);
    337             if (changeFlags != null && changeFlags.isAllTrue()) {
    338                 return;
    339             }
    340 
    341             // this means a new resource folder was added or removed, which can impact the
    342             // locale list.
    343             if (changeFlags == null) {
    344                 changeFlags = new ChangeFlags();
    345                 mProjectFlags.put(project, changeFlags);
    346             }
    347 
    348             changeFlags.localeList = true;
    349         }
    350 
    351         @Override
    352         public void fileChanged(IProject project, ResourceFile file, int eventType) {
    353             // if this project has already been marked as modified, we do nothing.
    354             ChangeFlags changeFlags = mProjectFlags.get(project);
    355             if (changeFlags != null && changeFlags.isAllTrue()) {
    356                 return;
    357             }
    358 
    359             // now check that the file is *NOT* a layout file (those automatically trigger a layout
    360             // reload and we don't want to do it twice.)
    361             Collection<ResourceType> resTypes = file.getResourceTypes();
    362 
    363             // it's unclear why but there has been cases of resTypes being empty!
    364             if (resTypes.size() > 0) {
    365                 // this is a resource change, that may require a layout redraw!
    366                 if (changeFlags == null) {
    367                     changeFlags = new ChangeFlags();
    368                     mProjectFlags.put(project, changeFlags);
    369                 }
    370 
    371                 changeFlags.resources = true;
    372             }
    373         }
    374     };
    375 }
    376