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
17package com.android.ide.eclipse.adt.internal.editors.layout;
18
19import com.android.SdkConstants;
20import com.android.annotations.NonNull;
21import com.android.annotations.Nullable;
22import com.android.ide.common.resources.ResourceFile;
23import com.android.ide.common.resources.ResourceFolder;
24import com.android.ide.eclipse.adt.AdtConstants;
25import com.android.ide.eclipse.adt.AdtPlugin;
26import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
27import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
28import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
29import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
30import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
31import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
32import com.android.ide.eclipse.adt.internal.sdk.Sdk;
33import com.android.resources.ResourceType;
34
35import org.eclipse.core.resources.IFile;
36import org.eclipse.core.resources.IMarkerDelta;
37import org.eclipse.core.resources.IProject;
38import org.eclipse.core.resources.IResourceDelta;
39import org.eclipse.core.runtime.CoreException;
40
41import java.util.ArrayList;
42import java.util.Collection;
43import java.util.HashMap;
44import java.util.Iterator;
45import java.util.List;
46import java.util.Map;
47import java.util.Map.Entry;
48import java.util.Set;
49
50/**
51 * Monitor for file changes that could trigger a layout redraw, or a UI update
52 */
53public 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