• 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.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 
isAllTrue()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          */
reloadLayout(ChangeFlags flags, boolean libraryModified)94         void reloadLayout(ChangeFlags flags, boolean libraryModified);
95     }
96 
97     /**
98      * Returns the single instance of {@link LayoutReloadMonitor}.
99      */
getMonitor()100     public static LayoutReloadMonitor getMonitor() {
101         return sThis;
102     }
103 
LayoutReloadMonitor()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      */
addListener(IProject project, ILayoutReloadListener listener)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      */
removeListener(IProject project, ILayoutReloadListener listener)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      */
removeListener(ILayoutReloadListener listener)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