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.ResourceManager; 25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; 26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener; 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.Set; 46 import java.util.Map.Entry; 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 public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { 175 // get the file's project 176 IProject project = file.getProject(); 177 178 boolean hasAndroidNature = false; 179 try { 180 hasAndroidNature = project.hasNature(AdtConstants.NATURE_DEFAULT); 181 } catch (CoreException e) { 182 // do nothing if the nature cannot be queried. 183 return; 184 } 185 186 if (hasAndroidNature) { 187 // project is an Android project, it's the one being affected 188 // directly by its own file change. 189 processFileChanged(file, project); 190 } else { 191 // check the projects depending on it, if they are Android project, update them. 192 IProject[] referencingProjects = project.getReferencingProjects(); 193 194 for (IProject p : referencingProjects) { 195 try { 196 hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT); 197 } catch (CoreException e) { 198 // do nothing if the nature cannot be queried. 199 continue; 200 } 201 202 if (hasAndroidNature) { 203 // the changed project is a dependency on an Android project, 204 // update the main project. 205 processFileChanged(file, p); 206 } 207 } 208 } 209 } 210 211 /** 212 * Processes a file change for a given project which may or may not be the file's project. 213 * @param file the changed file 214 * @param project the project impacted by the file change. 215 */ 216 private void processFileChanged(IFile file, IProject project) { 217 // if this project has already been marked as modified, we do nothing. 218 ChangeFlags changeFlags = mProjectFlags.get(project); 219 if (changeFlags != null && changeFlags.isAllTrue()) { 220 return; 221 } 222 223 // here we only care about code change (so change for .class files). 224 // Resource changes is handled by the IResourceListener. 225 if (AdtConstants.EXT_CLASS.equals(file.getFileExtension())) { 226 if (file.getName().matches("R[\\$\\.](.*)")) { 227 // this is a R change! 228 if (changeFlags == null) { 229 changeFlags = new ChangeFlags(); 230 mProjectFlags.put(project, changeFlags); 231 } 232 233 changeFlags.rClass = true; 234 } else { 235 // this is a code change! 236 if (changeFlags == null) { 237 changeFlags = new ChangeFlags(); 238 mProjectFlags.put(project, changeFlags); 239 } 240 241 changeFlags.code = true; 242 } 243 } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()) && 244 file.getParent().equals(project)) { 245 // this is a manifest change! 246 if (changeFlags == null) { 247 changeFlags = new ChangeFlags(); 248 mProjectFlags.put(project, changeFlags); 249 } 250 251 changeFlags.manifest = true; 252 } 253 } 254 }; 255 256 /** 257 * Implementation of the {@link IResourceEventListener} as an internal class so that the methods 258 * do not appear in the public API of {@link LayoutReloadMonitor}. 259 */ 260 private IResourceEventListener mResourceEventListener = new IResourceEventListener() { 261 /* 262 * Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a 263 * resource change event. This is called once, while fileChanged can be 264 * called several times. 265 * 266 */ 267 public void resourceChangeEventStart() { 268 // nothing to be done here, it all happens in the resourceChangeEventEnd 269 } 270 271 /* 272 * Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource 273 * change event. This is where we notify the listeners. 274 */ 275 public void resourceChangeEventEnd() { 276 // for each IProject that was changed, we notify all the listeners. 277 for (Entry<IProject, ChangeFlags> entry : mProjectFlags.entrySet()) { 278 IProject project = entry.getKey(); 279 280 // notify the project itself. 281 notifyForProject(project, entry.getValue(), false); 282 283 // check if the project is a library, and if it is search for what other 284 // project depends on this one (directly or not) 285 ProjectState state = Sdk.getProjectState(project); 286 if (state != null && state.isLibrary()) { 287 Set<ProjectState> mainProjects = Sdk.getMainProjectsFor(project); 288 for (ProjectState mainProject : mainProjects) { 289 // always give the changeflag of the modified project. 290 notifyForProject(mainProject.getProject(), entry.getValue(), true); 291 } 292 } 293 } 294 295 // empty the list. 296 mProjectFlags.clear(); 297 } 298 299 /** 300 * Notifies the listeners for a given project. 301 * @param project the project for which the listeners must be notified 302 * @param flags the change flags to pass to the listener 303 * @param libraryChanged a flag indicating if the change flags are for the give project, 304 * or if they are for a library dependency. 305 */ 306 private void notifyForProject(IProject project, ChangeFlags flags, 307 boolean libraryChanged) { 308 synchronized (mListenerMap) { 309 List<ILayoutReloadListener> listeners = mListenerMap.get(project); 310 311 if (listeners != null) { 312 for (ILayoutReloadListener listener : listeners) { 313 try { 314 listener.reloadLayout(flags, libraryChanged); 315 } catch (Throwable t) { 316 AdtPlugin.log(t, "Failed to call ILayoutReloadListener.reloadLayout"); 317 } 318 } 319 } 320 } 321 } 322 }; 323 324 /** 325 * Implementation of the {@link IResourceListener} as an internal class so that the methods 326 * do not appear in the public API of {@link LayoutReloadMonitor}. 327 */ 328 private IResourceListener mResourceListener = new IResourceListener() { 329 330 public void folderChanged(IProject project, ResourceFolder folder, int eventType) { 331 // if this project has already been marked as modified, we do nothing. 332 ChangeFlags changeFlags = mProjectFlags.get(project); 333 if (changeFlags != null && changeFlags.isAllTrue()) { 334 return; 335 } 336 337 // this means a new resource folder was added or removed, which can impact the 338 // locale list. 339 if (changeFlags == null) { 340 changeFlags = new ChangeFlags(); 341 mProjectFlags.put(project, changeFlags); 342 } 343 344 changeFlags.localeList = true; 345 } 346 347 public void fileChanged(IProject project, ResourceFile file, int eventType) { 348 // if this project has already been marked as modified, we do nothing. 349 ChangeFlags changeFlags = mProjectFlags.get(project); 350 if (changeFlags != null && changeFlags.isAllTrue()) { 351 return; 352 } 353 354 // now check that the file is *NOT* a layout file (those automatically trigger a layout 355 // reload and we don't want to do it twice.) 356 Collection<ResourceType> resTypes = file.getResourceTypes(); 357 358 // it's unclear why but there has been cases of resTypes being empty! 359 if (resTypes.size() > 0) { 360 // this is a resource change, that may require a layout redraw! 361 if (changeFlags == null) { 362 changeFlags = new ChangeFlags(); 363 mProjectFlags.put(project, changeFlags); 364 } 365 366 changeFlags.resources = true; 367 } 368 } 369 }; 370 } 371