• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ResourceManager;
25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
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.Set;
46 import java.util.Map.Entry;
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 
isAllTrue()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          */
reloadLayout(ChangeFlags flags, boolean libraryModified)92         void reloadLayout(ChangeFlags flags, boolean libraryModified);
93     }
94 
95     /**
96      * Returns the single instance of {@link LayoutReloadMonitor}.
97      */
getMonitor()98     public static LayoutReloadMonitor getMonitor() {
99         return sThis;
100     }
101 
LayoutReloadMonitor()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      */
addListener(IProject project, ILayoutReloadListener listener)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      */
removeListener(IProject project, ILayoutReloadListener listener)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      */
removeListener(ILayoutReloadListener listener)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         public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
175             // get the file's project
176             IProject project = file.getProject();
177 
178             boolean hasAndroidNature = false;
179             try {
180                 hasAndroidNature = project.hasNature(AdtConstants.NATURE_DEFAULT);
181             } catch (CoreException e) {
182                 // do nothing if the nature cannot be queried.
183                 return;
184             }
185 
186             if (hasAndroidNature) {
187                 // project is an Android project, it's the one being affected
188                 // directly by its own file change.
189                 processFileChanged(file, project);
190             } else {
191                 // check the projects depending on it, if they are Android project, update them.
192                 IProject[] referencingProjects = project.getReferencingProjects();
193 
194                 for (IProject p : referencingProjects) {
195                     try {
196                         hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT);
197                     } catch (CoreException e) {
198                         // do nothing if the nature cannot be queried.
199                         continue;
200                     }
201 
202                     if (hasAndroidNature) {
203                         // the changed project is a dependency on an Android project,
204                         // update the main project.
205                         processFileChanged(file, p);
206                     }
207                 }
208             }
209         }
210 
211         /**
212          * Processes a file change for a given project which may or may not be the file's project.
213          * @param file the changed file
214          * @param project the project impacted by the file change.
215          */
216         private void processFileChanged(IFile file, IProject project) {
217             // if this project has already been marked as modified, we do nothing.
218             ChangeFlags changeFlags = mProjectFlags.get(project);
219             if (changeFlags != null && changeFlags.isAllTrue()) {
220                 return;
221             }
222 
223             // here we only care about code change (so change for .class files).
224             // Resource changes is handled by the IResourceListener.
225             if (AdtConstants.EXT_CLASS.equals(file.getFileExtension())) {
226                 if (file.getName().matches("R[\\$\\.](.*)")) {
227                     // this is a R change!
228                     if (changeFlags == null) {
229                         changeFlags = new ChangeFlags();
230                         mProjectFlags.put(project, changeFlags);
231                     }
232 
233                     changeFlags.rClass = true;
234                 } else {
235                     // this is a code change!
236                     if (changeFlags == null) {
237                         changeFlags = new ChangeFlags();
238                         mProjectFlags.put(project, changeFlags);
239                     }
240 
241                     changeFlags.code = true;
242                 }
243             } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()) &&
244                     file.getParent().equals(project)) {
245                 // this is a manifest change!
246                 if (changeFlags == null) {
247                     changeFlags = new ChangeFlags();
248                     mProjectFlags.put(project, changeFlags);
249                 }
250 
251                 changeFlags.manifest = true;
252             }
253         }
254     };
255 
256     /**
257      * Implementation of the {@link IResourceEventListener} as an internal class so that the methods
258      * do not appear in the public API of {@link LayoutReloadMonitor}.
259      */
260     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
261         /*
262          * Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a
263          * resource change event. This is called once, while fileChanged can be
264          * called several times.
265          *
266          */
267         public void resourceChangeEventStart() {
268             // nothing to be done here, it all happens in the resourceChangeEventEnd
269         }
270 
271         /*
272          * Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource
273          * change event. This is where we notify the listeners.
274          */
275         public void resourceChangeEventEnd() {
276             // for each IProject that was changed, we notify all the listeners.
277             for (Entry<IProject, ChangeFlags> entry : mProjectFlags.entrySet()) {
278                 IProject project = entry.getKey();
279 
280                 // notify the project itself.
281                 notifyForProject(project, entry.getValue(), false);
282 
283                 // check if the project is a library, and if it is search for what other
284                 // project depends on this one (directly or not)
285                 ProjectState state = Sdk.getProjectState(project);
286                 if (state != null && state.isLibrary()) {
287                     Set<ProjectState> mainProjects = Sdk.getMainProjectsFor(project);
288                     for (ProjectState mainProject : mainProjects) {
289                         // always give the changeflag of the modified project.
290                         notifyForProject(mainProject.getProject(), entry.getValue(), true);
291                     }
292                 }
293             }
294 
295             // empty the list.
296             mProjectFlags.clear();
297         }
298 
299         /**
300          * Notifies the listeners for a given project.
301          * @param project the project for which the listeners must be notified
302          * @param flags the change flags to pass to the listener
303          * @param libraryChanged a flag indicating if the change flags are for the give project,
304          * or if they are for a library dependency.
305          */
306         private void notifyForProject(IProject project, ChangeFlags flags,
307                 boolean libraryChanged) {
308             synchronized (mListenerMap) {
309                 List<ILayoutReloadListener> listeners = mListenerMap.get(project);
310 
311                 if (listeners != null) {
312                     for (ILayoutReloadListener listener : listeners) {
313                         try {
314                             listener.reloadLayout(flags, libraryChanged);
315                         } catch (Throwable t) {
316                             AdtPlugin.log(t, "Failed to call ILayoutReloadListener.reloadLayout");
317                         }
318                     }
319                 }
320             }
321         }
322     };
323 
324     /**
325      * Implementation of the {@link IResourceListener} as an internal class so that the methods
326      * do not appear in the public API of {@link LayoutReloadMonitor}.
327      */
328     private IResourceListener mResourceListener = new IResourceListener() {
329 
330         public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
331             // if this project has already been marked as modified, we do nothing.
332             ChangeFlags changeFlags = mProjectFlags.get(project);
333             if (changeFlags != null && changeFlags.isAllTrue()) {
334                 return;
335             }
336 
337             // this means a new resource folder was added or removed, which can impact the
338             // locale list.
339             if (changeFlags == null) {
340                 changeFlags = new ChangeFlags();
341                 mProjectFlags.put(project, changeFlags);
342             }
343 
344             changeFlags.localeList = true;
345         }
346 
347         public void fileChanged(IProject project, ResourceFile file, int eventType) {
348             // if this project has already been marked as modified, we do nothing.
349             ChangeFlags changeFlags = mProjectFlags.get(project);
350             if (changeFlags != null && changeFlags.isAllTrue()) {
351                 return;
352             }
353 
354             // now check that the file is *NOT* a layout file (those automatically trigger a layout
355             // reload and we don't want to do it twice.)
356             Collection<ResourceType> resTypes = file.getResourceTypes();
357 
358             // it's unclear why but there has been cases of resTypes being empty!
359             if (resTypes.size() > 0) {
360                 // this is a resource change, that may require a layout redraw!
361                 if (changeFlags == null) {
362                     changeFlags = new ChangeFlags();
363                     mProjectFlags.put(project, changeFlags);
364                 }
365 
366                 changeFlags.resources = true;
367             }
368         }
369     };
370 }
371