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.GlobalProjectMonitor.IFileListener; 25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener; 26 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 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.Map.Entry; 46 import java.util.Set; 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 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 */ 92 void reloadLayout(ChangeFlags flags, boolean libraryModified); 93 } 94 95 /** 96 * Returns the single instance of {@link LayoutReloadMonitor}. 97 */ 98 public static LayoutReloadMonitor getMonitor() { 99 return sThis; 100 } 101 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 */ 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 */ 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 */ 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 @Override 175 public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { 176 // get the file's project 177 IProject project = file.getProject(); 178 179 boolean hasAndroidNature = false; 180 try { 181 hasAndroidNature = project.hasNature(AdtConstants.NATURE_DEFAULT); 182 } catch (CoreException e) { 183 // do nothing if the nature cannot be queried. 184 return; 185 } 186 187 if (hasAndroidNature) { 188 // project is an Android project, it's the one being affected 189 // directly by its own file change. 190 processFileChanged(file, project); 191 } else { 192 // check the projects depending on it, if they are Android project, update them. 193 IProject[] referencingProjects = project.getReferencingProjects(); 194 195 for (IProject p : referencingProjects) { 196 try { 197 hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT); 198 } catch (CoreException e) { 199 // do nothing if the nature cannot be queried. 200 continue; 201 } 202 203 if (hasAndroidNature) { 204 // the changed project is a dependency on an Android project, 205 // update the main project. 206 processFileChanged(file, p); 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) { 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 (AdtConstants.EXT_CLASS.equals(file.getFileExtension())) { 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