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.SdkConstants;
     20 import com.android.annotations.NonNull;
     21 import com.android.annotations.Nullable;
     22 import com.android.ide.common.resources.ResourceFile;
     23 import com.android.ide.common.resources.ResourceFolder;
     24 import com.android.ide.eclipse.adt.AdtConstants;
     25 import com.android.ide.eclipse.adt.AdtPlugin;
     26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
     27 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
     28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
     29 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     30 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
     31 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     32 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     33 import com.android.resources.ResourceType;
     34 
     35 import org.eclipse.core.resources.IFile;
     36 import org.eclipse.core.resources.IMarkerDelta;
     37 import org.eclipse.core.resources.IProject;
     38 import org.eclipse.core.resources.IResourceDelta;
     39 import org.eclipse.core.runtime.CoreException;
     40 
     41 import java.util.ArrayList;
     42 import java.util.Collection;
     43 import java.util.HashMap;
     44 import java.util.Iterator;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.Map.Entry;
     48 import java.util.Set;
     49 
     50 /**
     51  * Monitor for file changes that could trigger a layout redraw, or a UI update
     52  */
     53 public final class LayoutReloadMonitor {
     54 
     55     // singleton, enforced by private constructor.
     56     private final static LayoutReloadMonitor sThis = new LayoutReloadMonitor();
     57 
     58     /**
     59      * Map of listeners by IProject.
     60      */
     61     private final Map<IProject, List<ILayoutReloadListener>> mListenerMap =
     62         new HashMap<IProject, List<ILayoutReloadListener>>();
     63 
     64     public final static class ChangeFlags {
     65         public boolean code = false;
     66         /** any non-layout resource changes */
     67         public boolean resources = false;
     68         public boolean rClass = false;
     69         public boolean localeList = false;
     70         public boolean manifest = false;
     71 
     72         boolean isAllTrue() {
     73             return code && resources && rClass && localeList && manifest;
     74         }
     75     }
     76 
     77     /**
     78      * List of projects having received a resource change.
     79      */
     80     private final Map<IProject, ChangeFlags> mProjectFlags = new HashMap<IProject, ChangeFlags>();
     81 
     82     /**
     83      * Classes which implement this interface provide a method to respond to resource changes
     84      * triggering a layout redraw
     85      */
     86     public interface ILayoutReloadListener {
     87         /**
     88          * Sent when the layout needs to be redrawn
     89          *
     90          * @param flags a {@link ChangeFlags} object indicating what type of resource changed.
     91          * @param libraryModified <code>true</code> if the changeFlags are not for the project
     92          * associated with the listener, but instead correspond to a library.
     93          */
     94         void reloadLayout(ChangeFlags flags, boolean libraryModified);
     95     }
     96 
     97     /**
     98      * Returns the single instance of {@link LayoutReloadMonitor}.
     99      */
    100     public static LayoutReloadMonitor getMonitor() {
    101         return sThis;
    102     }
    103 
    104     private LayoutReloadMonitor() {
    105         // listen to resource changes. Used for non-layout resource (trigger a redraw), or
    106         // any resource folder (trigger a locale list refresh)
    107         ResourceManager.getInstance().addListener(mResourceListener);
    108 
    109         // also listen for .class file changed in case the layout has custom view classes.
    110         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    111         monitor.addFileListener(mFileListener,
    112                 IResourceDelta.ADDED | IResourceDelta.CHANGED | IResourceDelta.REMOVED);
    113 
    114         monitor.addResourceEventListener(mResourceEventListener);
    115     }
    116 
    117     /**
    118      * Adds a listener for a given {@link IProject}.
    119      * @param project
    120      * @param listener
    121      */
    122     public void addListener(IProject project, ILayoutReloadListener listener) {
    123         synchronized (mListenerMap) {
    124             List<ILayoutReloadListener> list = mListenerMap.get(project);
    125             if (list == null) {
    126                 list = new ArrayList<ILayoutReloadListener>();
    127                 mListenerMap.put(project, list);
    128             }
    129 
    130             list.add(listener);
    131         }
    132     }
    133 
    134     /**
    135      * Removes a listener for a given {@link IProject}.
    136      */
    137     public void removeListener(IProject project, ILayoutReloadListener listener) {
    138         synchronized (mListenerMap) {
    139             List<ILayoutReloadListener> list = mListenerMap.get(project);
    140             if (list != null) {
    141                 list.remove(listener);
    142             }
    143         }
    144     }
    145 
    146     /**
    147      * Removes a listener, no matter which {@link IProject} it was associated with.
    148      */
    149     public void removeListener(ILayoutReloadListener listener) {
    150         synchronized (mListenerMap) {
    151 
    152             for (List<ILayoutReloadListener> list : mListenerMap.values()) {
    153                 Iterator<ILayoutReloadListener> it = list.iterator();
    154                 while (it.hasNext()) {
    155                     ILayoutReloadListener i = it.next();
    156                     if (i == listener) {
    157                         it.remove();
    158                     }
    159                 }
    160             }
    161         }
    162     }
    163 
    164     /**
    165      * Implementation of the {@link IFileListener} as an internal class so that the methods
    166      * do not appear in the public API of {@link LayoutReloadMonitor}.
    167      *
    168      * This is only to detect code and manifest change. Resource changes (located in res/)
    169      * is done through {@link #mResourceListener}.
    170      */
    171     private IFileListener mFileListener = new IFileListener() {
    172         /*
    173          * Callback for IFileListener. Called when a file changed.
    174          * This records the changes for each project, but does not notify listeners.
    175          */
    176         @Override
    177         public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
    178                 int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
    179             // This listener only cares about .class files and AndroidManifest.xml files
    180             if (!(SdkConstants.EXT_CLASS.equals(extension)
    181                     || SdkConstants.EXT_XML.equals(extension)
    182                         && SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()))) {
    183                 return;
    184             }
    185 
    186             // get the file's project
    187             IProject project = file.getProject();
    188 
    189             if (isAndroidProject) {
    190                 // project is an Android project, it's the one being affected
    191                 // directly by its own file change.
    192                 processFileChanged(file, project, extension);
    193             } else {
    194                 // check the projects depending on it, if they are Android project, update them.
    195                 IProject[] referencingProjects = project.getReferencingProjects();
    196 
    197                 for (IProject p : referencingProjects) {
    198                     try {
    199                         boolean hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT);
    200                         if (hasAndroidNature) {
    201                             // the changed project is a dependency on an Android project,
    202                             // update the main project.
    203                             processFileChanged(file, p, extension);
    204                         }
    205                     } catch (CoreException e) {
    206                         // do nothing if the nature cannot be queried.
    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, String extension) {
    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 (SdkConstants.EXT_CLASS.equals(extension)) {
    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