1 /*
2  * Copyright (C) 2007 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.resources.manager;
18 
19 import com.android.annotations.NonNull;
20 import com.android.annotations.Nullable;
21 import com.android.ide.common.resources.ResourceFile;
22 import com.android.ide.common.resources.ResourceFolder;
23 import com.android.ide.eclipse.adt.AdtConstants;
24 import com.android.ide.eclipse.adt.AdtPlugin;
25 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
26 
27 import org.eclipse.core.resources.IFile;
28 import org.eclipse.core.resources.IFolder;
29 import org.eclipse.core.resources.IMarkerDelta;
30 import org.eclipse.core.resources.IProject;
31 import org.eclipse.core.resources.IResource;
32 import org.eclipse.core.resources.IResourceChangeEvent;
33 import org.eclipse.core.resources.IResourceChangeListener;
34 import org.eclipse.core.resources.IResourceDelta;
35 import org.eclipse.core.resources.IResourceDeltaVisitor;
36 import org.eclipse.core.resources.IWorkspace;
37 import org.eclipse.core.resources.IWorkspaceRoot;
38 import org.eclipse.core.runtime.CoreException;
39 import org.eclipse.core.runtime.IPath;
40 import org.eclipse.jdt.core.IJavaModel;
41 import org.eclipse.jdt.core.IJavaProject;
42 import org.eclipse.jdt.core.JavaCore;
43 
44 import java.util.ArrayList;
45 
46 /**
47  * The Global Project Monitor tracks project file changes, and forward them to simple project,
48  * file, and folder listeners.
49  * Those listeners can be setup with masks to listen to particular events.
50  * <p/>
51  * To track project resource changes, use the monitor in the {@link ResourceManager}. It is more
52  * efficient and while the global ProjectMonitor can track any file, deleted resource files
53  * cannot be matched to previous {@link ResourceFile} or {@link ResourceFolder} objects by the
54  * time the listeners get the event notifications.
55  *
56  * @see IProjectListener
57  * @see IFolderListener
58  * @see IFileListener
59  */
60 public final class GlobalProjectMonitor {
61 
62     private final static GlobalProjectMonitor sThis = new GlobalProjectMonitor();
63 
64     /**
65      * Classes which implement this interface provide a method that deals
66      * with file change events.
67      */
68     public interface IFileListener {
69         /**
70          * Sent when a file changed.
71          *
72          * @param file The file that changed.
73          * @param markerDeltas The marker deltas for the file.
74          * @param kind The change kind. This is equivalent to
75          *            {@link IResourceDelta#accept(IResourceDeltaVisitor)}
76          * @param extension the extension of the file or null if the file does
77          *            not have an extension
78          * @param flags the {@link IResourceDelta#getFlags()} value with details
79          *            on what changed in the file
80          * @param isAndroidProject whether the parent project is an Android Project
81          */
fileChanged(@onNull IFile file, @NonNull IMarkerDelta[] markerDeltas, int kind, @Nullable String extension, int flags, boolean isAndroidProject)82         public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
83                 int kind, @Nullable String extension, int flags, boolean isAndroidProject);
84     }
85 
86     /**
87      * Classes which implements this interface provide methods dealing with project events.
88      */
89     public interface IProjectListener {
90         /**
91          * Sent for each opened android project at the time the listener is put in place.
92          * @param project the opened project.
93          */
projectOpenedWithWorkspace(IProject project)94         public void projectOpenedWithWorkspace(IProject project);
95 
96         /**
97          * Sent once after all Android projects have been opened,
98          * at the time the listener is put in place.
99          * <p/>
100          * This is called after {@link #projectOpenedWithWorkspace(IProject)} has
101          * been called on all known Android projects.
102          */
allProjectsOpenedWithWorkspace()103         public void allProjectsOpenedWithWorkspace();
104 
105         /**
106          * Sent when a project is opened.
107          * @param project the project being opened.
108          */
projectOpened(IProject project)109         public void projectOpened(IProject project);
110 
111         /**
112          * Sent when a project is closed.
113          * @param project the project being closed.
114          */
projectClosed(IProject project)115         public void projectClosed(IProject project);
116 
117         /**
118          * Sent when a project is deleted.
119          * @param project the project about to be deleted.
120          */
projectDeleted(IProject project)121         public void projectDeleted(IProject project);
122 
123         /**
124          * Sent when a project is renamed. During a project rename
125          * {@link #projectDeleted(IProject)} and {@link #projectOpened(IProject)} are also called.
126          * This is called last.
127          *
128          * @param project the new {@link IProject} object.
129          * @param from the path of the project before the rename action.
130          */
projectRenamed(IProject project, IPath from)131         public void projectRenamed(IProject project, IPath from);
132     }
133 
134     /**
135      * Classes which implement this interface provide a method that deals
136      * with folder change events
137      */
138     public interface IFolderListener {
139         /**
140          * Sent when a folder changed.
141          * @param folder The file that was changed
142          * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()}
143          * @param isAndroidProject whether the parent project is an Android Project
144          */
folderChanged(IFolder folder, int kind, boolean isAndroidProject)145         public void folderChanged(IFolder folder, int kind, boolean isAndroidProject);
146     }
147 
148     /**
149      * Interface for a listener to be notified when resource change event starts and ends.
150      */
151     public interface IResourceEventListener {
resourceChangeEventStart()152         public void resourceChangeEventStart();
resourceChangeEventEnd()153         public void resourceChangeEventEnd();
154     }
155 
156     /**
157      * Interface for a listener that gets passed the raw delta without processing.
158      */
159     public interface IRawDeltaListener {
visitDelta(IResourceDelta delta)160         public void visitDelta(IResourceDelta delta);
161     }
162 
163     /**
164      * Base listener bundle to associate a listener to an event mask.
165      */
166     private static class ListenerBundle {
167         /** Mask value to accept all events */
168         public final static int MASK_NONE = -1;
169 
170         /**
171          * Event mask. Values accepted are IResourceDelta.###
172          * @see IResourceDelta#ADDED
173          * @see IResourceDelta#REMOVED
174          * @see IResourceDelta#CHANGED
175          * @see IResourceDelta#ADDED_PHANTOM
176          * @see IResourceDelta#REMOVED_PHANTOM
177          * */
178         int kindMask;
179     }
180 
181     /**
182      * Listener bundle for file event.
183      */
184     private static class FileListenerBundle extends ListenerBundle {
185 
186         /** The file listener */
187         IFileListener listener;
188     }
189 
190     /**
191      * Listener bundle for folder event.
192      */
193     private static class FolderListenerBundle extends ListenerBundle {
194         /** The file listener */
195         IFolderListener listener;
196     }
197 
198     private final ArrayList<FileListenerBundle> mFileListeners =
199         new ArrayList<FileListenerBundle>();
200 
201     private final ArrayList<FolderListenerBundle> mFolderListeners =
202         new ArrayList<FolderListenerBundle>();
203 
204     private final ArrayList<IProjectListener> mProjectListeners = new ArrayList<IProjectListener>();
205 
206     private final ArrayList<IResourceEventListener> mEventListeners =
207         new ArrayList<IResourceEventListener>();
208 
209     private final ArrayList<IRawDeltaListener> mRawDeltaListeners =
210         new ArrayList<IRawDeltaListener>();
211 
212     private IWorkspace mWorkspace;
213 
214     private boolean mIsAndroidProject;
215 
216     /**
217      * Delta visitor for resource changes.
218      */
219     private final class DeltaVisitor implements IResourceDeltaVisitor {
220 
221         @Override
visit(IResourceDelta delta)222         public boolean visit(IResourceDelta delta) {
223             // Find the other resource listeners to notify
224             IResource r = delta.getResource();
225             int type = r.getType();
226             if (type == IResource.FILE) {
227                 int kind = delta.getKind();
228                 // notify the listeners.
229                 for (FileListenerBundle bundle : mFileListeners) {
230                     if (bundle.kindMask == ListenerBundle.MASK_NONE
231                             || (bundle.kindMask & kind) != 0) {
232                         try {
233                             bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind,
234                                     r.getFileExtension(), delta.getFlags(), mIsAndroidProject);
235                         } catch (Throwable t) {
236                             AdtPlugin.log(t,"Failed to call IFileListener.fileChanged");
237                         }
238                     }
239                 }
240                 return false;
241             } else if (type == IResource.FOLDER) {
242                 int kind = delta.getKind();
243                 // notify the listeners.
244                 for (FolderListenerBundle bundle : mFolderListeners) {
245                     if (bundle.kindMask == ListenerBundle.MASK_NONE
246                             || (bundle.kindMask & kind) != 0) {
247                         try {
248                             bundle.listener.folderChanged((IFolder)r, kind, mIsAndroidProject);
249                         } catch (Throwable t) {
250                             AdtPlugin.log(t,"Failed to call IFileListener.folderChanged");
251                         }
252                     }
253                 }
254                 return true;
255             } else if (type == IResource.PROJECT) {
256                 IProject project = (IProject)r;
257 
258                 try {
259                     mIsAndroidProject = project.hasNature(AdtConstants.NATURE_DEFAULT);
260                 } catch (CoreException e) {
261                     // this can only happen if the project does not exist or is not open, neither
262                     // of which can happen here since we are processing changes in the project
263                     // or at worst a project post-open event.
264                     return false;
265                 }
266 
267                 if (mIsAndroidProject == false) {
268                     // for non android project, skip the project listeners but return true
269                     // to visit the children and notify the IFileListeners
270                     return true;
271                 }
272 
273                 int flags = delta.getFlags();
274 
275                 if ((flags & IResourceDelta.OPEN) != 0) {
276                     // the project is opening or closing.
277 
278                     if (project.isOpen()) {
279                         // notify the listeners.
280                         for (IProjectListener pl : mProjectListeners) {
281                             try {
282                                 pl.projectOpened(project);
283                             } catch (Throwable t) {
284                                 AdtPlugin.log(t,"Failed to call IProjectListener.projectOpened");
285                             }
286                         }
287                     } else {
288                         // notify the listeners.
289                         for (IProjectListener pl : mProjectListeners) {
290                             try {
291                                 pl.projectClosed(project);
292                             } catch (Throwable t) {
293                                 AdtPlugin.log(t,"Failed to call IProjectListener.projectClosed");
294                             }
295                         }
296                     }
297 
298                     if ((flags & IResourceDelta.MOVED_FROM) != 0) {
299                         IPath from = delta.getMovedFromPath();
300                         // notify the listeners.
301                         for (IProjectListener pl : mProjectListeners) {
302                             try {
303                                 pl.projectRenamed(project, from);
304                             } catch (Throwable t) {
305                                 AdtPlugin.log(t,"Failed to call IProjectListener.projectRenamed");
306                             }
307                         }
308                     }
309                 }
310             }
311 
312             return true;
313         }
314     }
315 
getMonitor()316     public static GlobalProjectMonitor getMonitor() {
317         return sThis;
318     }
319 
320 
321     /**
322      * Starts the resource monitoring.
323      * @param ws The current workspace.
324      * @return The monitor object.
325      */
startMonitoring(IWorkspace ws)326     public static GlobalProjectMonitor startMonitoring(IWorkspace ws) {
327         if (sThis != null) {
328             ws.addResourceChangeListener(sThis.mResourceChangeListener,
329                     IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE);
330             sThis.mWorkspace = ws;
331         }
332         return sThis;
333     }
334 
335     /**
336      * Stops the resource monitoring.
337      * @param ws The current workspace.
338      */
stopMonitoring(IWorkspace ws)339     public static void stopMonitoring(IWorkspace ws) {
340         if (sThis != null) {
341             ws.removeResourceChangeListener(sThis.mResourceChangeListener);
342 
343             synchronized (sThis) {
344                 sThis.mFileListeners.clear();
345                 sThis.mProjectListeners.clear();
346             }
347         }
348     }
349 
350     /**
351      * Adds a file listener.
352      * @param listener The listener to receive the events.
353      * @param kindMask The event mask to filter out specific events.
354      * {@link ListenerBundle#MASK_NONE} will forward all events.
355      * See {@link ListenerBundle#kindMask} for more values.
356      */
addFileListener(IFileListener listener, int kindMask)357     public synchronized void addFileListener(IFileListener listener, int kindMask) {
358         FileListenerBundle bundle = new FileListenerBundle();
359         bundle.listener = listener;
360         bundle.kindMask = kindMask;
361 
362         mFileListeners.add(bundle);
363     }
364 
365     /**
366      * Removes an existing file listener.
367      * @param listener the listener to remove.
368      */
removeFileListener(IFileListener listener)369     public synchronized void removeFileListener(IFileListener listener) {
370         for (int i = 0 ; i < mFileListeners.size() ; i++) {
371             FileListenerBundle bundle = mFileListeners.get(i);
372             if (bundle.listener == listener) {
373                 mFileListeners.remove(i);
374                 return;
375             }
376         }
377     }
378 
379     /**
380      * Adds a folder listener.
381      * @param listener The listener to receive the events.
382      * @param kindMask The event mask to filter out specific events.
383      * {@link ListenerBundle#MASK_NONE} will forward all events.
384      * See {@link ListenerBundle#kindMask} for more values.
385      */
addFolderListener(IFolderListener listener, int kindMask)386     public synchronized void addFolderListener(IFolderListener listener, int kindMask) {
387         FolderListenerBundle bundle = new FolderListenerBundle();
388         bundle.listener = listener;
389         bundle.kindMask = kindMask;
390 
391         mFolderListeners.add(bundle);
392     }
393 
394     /**
395      * Removes an existing folder listener.
396      * @param listener the listener to remove.
397      */
removeFolderListener(IFolderListener listener)398     public synchronized void removeFolderListener(IFolderListener listener) {
399         for (int i = 0 ; i < mFolderListeners.size() ; i++) {
400             FolderListenerBundle bundle = mFolderListeners.get(i);
401             if (bundle.listener == listener) {
402                 mFolderListeners.remove(i);
403                 return;
404             }
405         }
406     }
407 
408     /**
409      * Adds a project listener.
410      * @param listener The listener to receive the events.
411      */
addProjectListener(IProjectListener listener)412     public synchronized void addProjectListener(IProjectListener listener) {
413         mProjectListeners.add(listener);
414 
415         // we need to look at the opened projects and give them to the listener.
416 
417         // get the list of opened android projects.
418         IWorkspaceRoot workspaceRoot = mWorkspace.getRoot();
419         IJavaModel javaModel = JavaCore.create(workspaceRoot);
420         IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(javaModel,
421                 null /*filter*/);
422 
423 
424         notifyResourceEventStart();
425 
426         for (IJavaProject androidProject : androidProjects) {
427             listener.projectOpenedWithWorkspace(androidProject.getProject());
428         }
429 
430         listener.allProjectsOpenedWithWorkspace();
431 
432         notifyResourceEventEnd();
433     }
434 
435     /**
436      * Removes an existing project listener.
437      * @param listener the listener to remove.
438      */
removeProjectListener(IProjectListener listener)439     public synchronized void removeProjectListener(IProjectListener listener) {
440         mProjectListeners.remove(listener);
441     }
442 
443     /**
444      * Adds a resource event listener.
445      * @param listener The listener to receive the events.
446      */
addResourceEventListener(IResourceEventListener listener)447     public synchronized void addResourceEventListener(IResourceEventListener listener) {
448         mEventListeners.add(listener);
449     }
450 
451     /**
452      * Removes an existing Resource Event listener.
453      * @param listener the listener to remove.
454      */
removeResourceEventListener(IResourceEventListener listener)455     public synchronized void removeResourceEventListener(IResourceEventListener listener) {
456         mEventListeners.remove(listener);
457     }
458 
459     /**
460      * Adds a raw delta listener.
461      * @param listener The listener to receive the deltas.
462      */
addRawDeltaListener(IRawDeltaListener listener)463     public synchronized void addRawDeltaListener(IRawDeltaListener listener) {
464         mRawDeltaListeners.add(listener);
465     }
466 
467     /**
468      * Removes an existing Raw Delta listener.
469      * @param listener the listener to remove.
470      */
removeRawDeltaListener(IRawDeltaListener listener)471     public synchronized void removeRawDeltaListener(IRawDeltaListener listener) {
472         mRawDeltaListeners.remove(listener);
473     }
474 
notifyResourceEventStart()475     private void notifyResourceEventStart() {
476         for (IResourceEventListener listener : mEventListeners) {
477             try {
478                 listener.resourceChangeEventStart();
479             } catch (Throwable t) {
480                 AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventStart");
481             }
482         }
483     }
484 
notifyResourceEventEnd()485     private void notifyResourceEventEnd() {
486         for (IResourceEventListener listener : mEventListeners) {
487             try {
488                 listener.resourceChangeEventEnd();
489             } catch (Throwable t) {
490                 AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventEnd");
491             }
492         }
493     }
494 
495     private IResourceChangeListener mResourceChangeListener = new IResourceChangeListener() {
496         /**
497          * Processes the workspace resource change events.
498          *
499          * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent)
500          */
501         @Override
502         public synchronized void resourceChanged(IResourceChangeEvent event) {
503             // notify the event listeners of a start.
504             notifyResourceEventStart();
505 
506             if (event.getType() == IResourceChangeEvent.PRE_DELETE) {
507                 // a project is being deleted. Lets get the project object and remove
508                 // its compiled resource list.
509                 IResource r = event.getResource();
510                 IProject project = r.getProject();
511 
512                 // notify the listeners.
513                 for (IProjectListener pl : mProjectListeners) {
514                     try {
515                         if (project.hasNature(AdtConstants.NATURE_DEFAULT)) {
516                             try {
517                                 pl.projectDeleted(project);
518                             } catch (Throwable t) {
519                                 AdtPlugin.log(t,"Failed to call IProjectListener.projectDeleted");
520                             }
521                         }
522                     } catch (CoreException e) {
523                         // just ignore this project.
524                     }
525                 }
526             } else {
527                 // this a regular resource change. We get the delta and go through it with a visitor.
528                 IResourceDelta delta = event.getDelta();
529 
530                 // notify the raw delta listeners
531                 for (IRawDeltaListener listener : mRawDeltaListeners) {
532                     listener.visitDelta(delta);
533                 }
534 
535                 DeltaVisitor visitor = new DeltaVisitor();
536                 try {
537                     delta.accept(visitor);
538                 } catch (CoreException e) {
539                 }
540             }
541 
542             // we're done, notify the event listeners.
543             notifyResourceEventEnd();
544         }
545     };
546 }
547