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