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.SdkConstants; 20 import com.android.ide.common.resources.FrameworkResources; 21 import com.android.ide.common.resources.ResourceFile; 22 import com.android.ide.common.resources.ResourceFolder; 23 import com.android.ide.common.resources.ResourceRepository; 24 import com.android.ide.common.resources.ScanningContext; 25 import com.android.ide.eclipse.adt.AdtConstants; 26 import com.android.ide.eclipse.adt.AdtPlugin; 27 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; 29 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IRawDeltaListener; 30 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 31 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 32 import com.android.ide.eclipse.adt.io.IFileWrapper; 33 import com.android.ide.eclipse.adt.io.IFolderWrapper; 34 import com.android.io.FolderWrapper; 35 import com.android.resources.ResourceFolderType; 36 import com.android.sdklib.IAndroidTarget; 37 38 import org.eclipse.core.resources.IContainer; 39 import org.eclipse.core.resources.IFile; 40 import org.eclipse.core.resources.IFolder; 41 import org.eclipse.core.resources.IMarkerDelta; 42 import org.eclipse.core.resources.IProject; 43 import org.eclipse.core.resources.IResource; 44 import org.eclipse.core.resources.IResourceDelta; 45 import org.eclipse.core.resources.IResourceDeltaVisitor; 46 import org.eclipse.core.resources.ResourcesPlugin; 47 import org.eclipse.core.runtime.CoreException; 48 import org.eclipse.core.runtime.IPath; 49 import org.eclipse.core.runtime.IStatus; 50 import org.eclipse.core.runtime.QualifiedName; 51 52 import java.util.ArrayList; 53 import java.util.Collection; 54 import java.util.HashMap; 55 import java.util.Map; 56 57 /** 58 * The ResourceManager tracks resources for all opened projects. 59 * <p/> 60 * It provide direct access to all the resources of a project as a {@link ProjectResources} 61 * object that allows accessing the resources through their file representation or as Android 62 * resources (similar to what is seen by an Android application). 63 * <p/> 64 * The ResourceManager automatically tracks file changes to update its internal representation 65 * of the resources so that they are always up to date. 66 * <p/> 67 * It also gives access to a monitor that is more resource oriented than the 68 * {@link GlobalProjectMonitor}. 69 * This monitor will let you track resource changes by giving you direct access to 70 * {@link ResourceFile}, or {@link ResourceFolder}. 71 * 72 * @see ProjectResources 73 */ 74 public final class ResourceManager { 75 public final static boolean DEBUG = false; 76 77 private final static ResourceManager sThis = new ResourceManager(); 78 79 /** 80 * Map associating project resource with project objects. 81 * <p/><b>All accesses must be inside a synchronized(mMap) block</b>, and do as a little as 82 * possible and <b>not call out to other classes</b>. 83 */ 84 private final Map<IProject, ProjectResources> mMap = 85 new HashMap<IProject, ProjectResources>(); 86 87 /** 88 * Interface to be notified of resource changes. 89 * 90 * @see ResourceManager#addListener(IResourceListener) 91 * @see ResourceManager#removeListener(IResourceListener) 92 */ 93 public interface IResourceListener { 94 /** 95 * Notification for resource file change. 96 * @param project the project of the file. 97 * @param file the {@link ResourceFile} representing the file. 98 * @param eventType the type of event. See {@link IResourceDelta}. 99 */ 100 void fileChanged(IProject project, ResourceFile file, int eventType); 101 /** 102 * Notification for resource folder change. 103 * @param project the project of the file. 104 * @param folder the {@link ResourceFolder} representing the folder. 105 * @param eventType the type of event. See {@link IResourceDelta}. 106 */ 107 void folderChanged(IProject project, ResourceFolder folder, int eventType); 108 } 109 110 private final ArrayList<IResourceListener> mListeners = new ArrayList<IResourceListener>(); 111 112 /** 113 * Sets up the resource manager with the global project monitor. 114 * @param monitor The global project monitor 115 */ 116 public static void setup(GlobalProjectMonitor monitor) { 117 monitor.addProjectListener(sThis.mProjectListener); 118 monitor.addRawDeltaListener(sThis.mRawDeltaListener); 119 120 CompiledResourcesMonitor.setupMonitor(monitor); 121 } 122 123 /** 124 * Returns the singleton instance. 125 */ 126 public static ResourceManager getInstance() { 127 return sThis; 128 } 129 130 /** 131 * Adds a new {@link IResourceListener} to be notified of resource changes. 132 * @param listener the listener to be added. 133 */ 134 public void addListener(IResourceListener listener) { 135 synchronized (mListeners) { 136 mListeners.add(listener); 137 } 138 } 139 140 /** 141 * Removes an {@link IResourceListener}, so that it's not notified of resource changes anymore. 142 * @param listener the listener to be removed. 143 */ 144 public void removeListener(IResourceListener listener) { 145 synchronized (mListeners) { 146 mListeners.remove(listener); 147 } 148 } 149 150 /** 151 * Returns the resources of a project. 152 * @param project The project 153 * @return a ProjectResources object 154 */ 155 public ProjectResources getProjectResources(IProject project) { 156 synchronized (mMap) { 157 ProjectResources resources = mMap.get(project); 158 159 if (resources == null) { 160 resources = ProjectResources.create(project); 161 mMap.put(project, resources); 162 } 163 164 return resources; 165 } 166 } 167 168 /** 169 * Update the resource repository with a delta 170 * 171 * @param delta the resource changed delta to process. 172 * @param context a context object with state for the current update, such 173 * as a place to stash errors encountered 174 */ 175 public void processDelta(IResourceDelta delta, IdeScanningContext context) { 176 doProcessDelta(delta, context); 177 178 // when a project is added to the workspace it is possible this is called before the 179 // repo is actually created so this will return null. 180 ResourceRepository repo = context.getRepository(); 181 if (repo != null) { 182 repo.postUpdateCleanUp(); 183 } 184 } 185 186 /** 187 * Update the resource repository with a delta 188 * 189 * @param delta the resource changed delta to process. 190 * @param context a context object with state for the current update, such 191 * as a place to stash errors encountered 192 */ 193 private void doProcessDelta(IResourceDelta delta, IdeScanningContext context) { 194 // Skip over deltas that don't fit our mask 195 int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED; 196 int kind = delta.getKind(); 197 if ( (mask & kind) == 0) { 198 return; 199 } 200 201 // Process this delta first as we need to make sure new folders are created before 202 // we process their content 203 IResource r = delta.getResource(); 204 int type = r.getType(); 205 206 if (type == IResource.FILE) { 207 context.startScanning(r); 208 updateFile((IFile)r, delta.getMarkerDeltas(), kind, context); 209 context.finishScanning(r); 210 } else if (type == IResource.FOLDER) { 211 updateFolder((IFolder)r, kind, context); 212 } // We only care about files and folders. 213 // Project deltas are handled by our project listener 214 215 // Now, process children recursively 216 IResourceDelta[] children = delta.getAffectedChildren(); 217 for (IResourceDelta child : children) { 218 processDelta(child, context); 219 } 220 } 221 222 /** 223 * Update a resource folder that we know about 224 * @param folder the folder that was updated 225 * @param kind the delta type (added/removed/updated) 226 */ 227 private void updateFolder(IFolder folder, int kind, IdeScanningContext context) { 228 ProjectResources resources; 229 230 final IProject project = folder.getProject(); 231 232 try { 233 if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 234 return; 235 } 236 } catch (CoreException e) { 237 // can't get the project nature? return! 238 return; 239 } 240 241 switch (kind) { 242 case IResourceDelta.ADDED: 243 // checks if the folder is under res. 244 IPath path = folder.getFullPath(); 245 246 // the path will be project/res/<something> 247 if (path.segmentCount() == 3) { 248 if (isInResFolder(path)) { 249 // get the project and its resource object. 250 synchronized (mMap) { 251 resources = mMap.get(project); 252 253 // if it doesn't exist, we create it. 254 if (resources == null) { 255 resources = ProjectResources.create(project); 256 mMap.put(project, resources); 257 } 258 } 259 260 ResourceFolder newFolder = resources.processFolder( 261 new IFolderWrapper(folder)); 262 if (newFolder != null) { 263 notifyListenerOnFolderChange(project, newFolder, kind); 264 } 265 } 266 } 267 break; 268 case IResourceDelta.CHANGED: 269 // only call the listeners. 270 synchronized (mMap) { 271 resources = mMap.get(folder.getProject()); 272 } 273 if (resources != null) { 274 ResourceFolder resFolder = resources.getResourceFolder(folder); 275 if (resFolder != null) { 276 notifyListenerOnFolderChange(project, resFolder, kind); 277 } 278 } 279 break; 280 case IResourceDelta.REMOVED: 281 synchronized (mMap) { 282 resources = mMap.get(folder.getProject()); 283 } 284 if (resources != null) { 285 // lets get the folder type 286 ResourceFolderType type = ResourceFolderType.getFolderType( 287 folder.getName()); 288 289 context.startScanning(folder); 290 ResourceFolder removedFolder = resources.removeFolder(type, 291 new IFolderWrapper(folder), context); 292 context.finishScanning(folder); 293 if (removedFolder != null) { 294 notifyListenerOnFolderChange(project, removedFolder, kind); 295 } 296 } 297 break; 298 } 299 } 300 301 /** 302 * Called when a delta indicates that a file has changed. Depending on the 303 * file being changed, and the type of change (ADDED, REMOVED, CHANGED), the 304 * file change is processed to update the resource manager data. 305 * 306 * @param file The file that changed. 307 * @param markerDeltas The marker deltas for the file. 308 * @param kind The change kind. This is equivalent to 309 * {@link IResourceDelta#accept(IResourceDeltaVisitor)} 310 * @param context a context object with state for the current update, such 311 * as a place to stash errors encountered 312 */ 313 private void updateFile(IFile file, IMarkerDelta[] markerDeltas, int kind, 314 ScanningContext context) { 315 final IProject project = file.getProject(); 316 317 try { 318 if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 319 return; 320 } 321 } catch (CoreException e) { 322 // can't get the project nature? return! 323 return; 324 } 325 326 // get the project resources 327 ProjectResources resources; 328 synchronized (mMap) { 329 resources = mMap.get(project); 330 } 331 332 if (resources == null) { 333 return; 334 } 335 336 // checks if the file is under res/something or bin/res/something 337 IPath path = file.getFullPath(); 338 339 if (path.segmentCount() == 4 || path.segmentCount() == 5) { 340 if (isInResFolder(path)) { 341 IContainer container = file.getParent(); 342 if (container instanceof IFolder) { 343 344 ResourceFolder folder = resources.getResourceFolder( 345 (IFolder)container); 346 347 // folder can be null as when the whole folder is deleted, the 348 // REMOVED event for the folder comes first. In this case, the 349 // folder will have taken care of things. 350 if (folder != null) { 351 ResourceFile resFile = folder.processFile( 352 new IFileWrapper(file), 353 ResourceHelper.getResourceDeltaKind(kind), context); 354 notifyListenerOnFileChange(project, resFile, kind); 355 } 356 } 357 } 358 } 359 } 360 361 /** 362 * Implementation of the {@link IProjectListener} as an internal class so that the methods 363 * do not appear in the public API of {@link ResourceManager}. 364 */ 365 private final IProjectListener mProjectListener = new IProjectListener() { 366 @Override 367 public void projectClosed(IProject project) { 368 synchronized (mMap) { 369 mMap.remove(project); 370 } 371 } 372 373 @Override 374 public void projectDeleted(IProject project) { 375 synchronized (mMap) { 376 mMap.remove(project); 377 } 378 } 379 380 @Override 381 public void projectOpened(IProject project) { 382 createProject(project); 383 } 384 385 @Override 386 public void projectOpenedWithWorkspace(IProject project) { 387 createProject(project); 388 } 389 390 @Override 391 public void allProjectsOpenedWithWorkspace() { 392 // nothing to do. 393 } 394 395 @Override 396 public void projectRenamed(IProject project, IPath from) { 397 // renamed project get a delete/open event too, so this can be ignored. 398 } 399 }; 400 401 /** 402 * Implementation of {@link IRawDeltaListener} as an internal class so that the methods 403 * do not appear in the public API of {@link ResourceManager}. Delta processing can be 404 * accessed through the {@link ResourceManager#visitDelta(IResourceDelta delta)} method. 405 */ 406 private final IRawDeltaListener mRawDeltaListener = new IRawDeltaListener() { 407 @Override 408 public void visitDelta(IResourceDelta workspaceDelta) { 409 // If we're auto-building, then PreCompilerBuilder will pass us deltas and 410 // they will be processed as part of the build. 411 if (isAutoBuilding()) { 412 return; 413 } 414 415 // When *not* auto building, we need to process the deltas immediately on save, 416 // even if the user is not building yet, such that for example resource ids 417 // are updated in the resource repositories so rendering etc. can work for 418 // those new ids. 419 420 IResourceDelta[] projectDeltas = workspaceDelta.getAffectedChildren(); 421 for (IResourceDelta delta : projectDeltas) { 422 if (delta.getResource() instanceof IProject) { 423 IProject project = (IProject) delta.getResource(); 424 425 try { 426 if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 427 continue; 428 } 429 } catch (CoreException e) { 430 // only happens if the project is closed or doesn't exist. 431 } 432 433 IdeScanningContext context = 434 new IdeScanningContext(getProjectResources(project), project, true); 435 436 processDelta(delta, context); 437 438 Collection<IProject> projects = context.getAaptRequestedProjects(); 439 if (projects != null) { 440 for (IProject p : projects) { 441 markAaptRequested(p); 442 } 443 } 444 } else { 445 AdtPlugin.log(IStatus.WARNING, "Unexpected delta type: %1$s", 446 delta.getResource().toString()); 447 } 448 } 449 } 450 }; 451 452 /** 453 * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists. 454 */ 455 public ResourceFolder getResourceFolder(IFile file) { 456 IContainer container = file.getParent(); 457 if (container.getType() == IResource.FOLDER) { 458 IFolder parent = (IFolder)container; 459 IProject project = file.getProject(); 460 461 ProjectResources resources = getProjectResources(project); 462 if (resources != null) { 463 return resources.getResourceFolder(parent); 464 } 465 } 466 467 return null; 468 } 469 470 /** 471 * Returns the {@link ResourceFolder} for the given folder or <code>null</code> if none exists. 472 */ 473 public ResourceFolder getResourceFolder(IFolder folder) { 474 IProject project = folder.getProject(); 475 476 ProjectResources resources = getProjectResources(project); 477 if (resources != null) { 478 return resources.getResourceFolder(folder); 479 } 480 481 return null; 482 } 483 484 /** 485 * Loads and returns the resources for a given {@link IAndroidTarget} 486 * @param androidTarget the target from which to load the framework resources 487 */ 488 public ResourceRepository loadFrameworkResources(IAndroidTarget androidTarget) { 489 String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES); 490 491 FolderWrapper frameworkRes = new FolderWrapper(osResourcesPath); 492 if (frameworkRes.exists()) { 493 FrameworkResources resources = new FrameworkResources(frameworkRes); 494 495 resources.loadResources(); 496 resources.loadPublicResources(AdtPlugin.getDefault()); 497 return resources; 498 } 499 500 return null; 501 } 502 503 /** 504 * Initial project parsing to gather resource info. 505 * @param project 506 */ 507 private void createProject(IProject project) { 508 if (project.isOpen()) { 509 synchronized (mMap) { 510 ProjectResources projectResources = mMap.get(project); 511 if (projectResources == null) { 512 projectResources = ProjectResources.create(project); 513 mMap.put(project, projectResources); 514 } 515 } 516 } 517 } 518 519 520 /** 521 * Returns true if the path is under /project/res/ 522 * @param path a workspace relative path 523 * @return true if the path is under /project res/ 524 */ 525 private boolean isInResFolder(IPath path) { 526 return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1)); 527 } 528 529 private void notifyListenerOnFolderChange(IProject project, ResourceFolder folder, 530 int eventType) { 531 synchronized (mListeners) { 532 for (IResourceListener listener : mListeners) { 533 try { 534 listener.folderChanged(project, folder, eventType); 535 } catch (Throwable t) { 536 AdtPlugin.log(t, 537 "Failed to execute ResourceManager.IResouceListener.folderChanged()"); //$NON-NLS-1$ 538 } 539 } 540 } 541 } 542 543 private void notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType) { 544 synchronized (mListeners) { 545 for (IResourceListener listener : mListeners) { 546 try { 547 listener.fileChanged(project, file, eventType); 548 } catch (Throwable t) { 549 AdtPlugin.log(t, 550 "Failed to execute ResourceManager.IResouceListener.fileChanged()"); //$NON-NLS-1$ 551 } 552 } 553 } 554 } 555 556 /** 557 * Private constructor to enforce singleton design. 558 */ 559 private ResourceManager() { 560 } 561 562 // debug only 563 @SuppressWarnings("unused") 564 private String getKindString(int kind) { 565 if (DEBUG) { 566 switch (kind) { 567 case IResourceDelta.ADDED: return "ADDED"; 568 case IResourceDelta.REMOVED: return "REMOVED"; 569 case IResourceDelta.CHANGED: return "CHANGED"; 570 } 571 } 572 573 return Integer.toString(kind); 574 } 575 576 /** 577 * Returns true if the Project > Build Automatically option is turned on 578 * (default). 579 * 580 * @return true if the Project > Build Automatically option is turned on 581 * (default). 582 */ 583 public static boolean isAutoBuilding() { 584 return ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding(); 585 } 586 587 /** Qualified name for the per-project persistent property "needs aapt" */ 588 private final static QualifiedName NEED_AAPT = new QualifiedName(AdtPlugin.PLUGIN_ID, 589 "aapt");//$NON-NLS-1$ 590 591 /** 592 * Mark the given project, and any projects which depend on it as a library 593 * project, as needing a full aapt build the next time the project is built. 594 * 595 * @param project the project to mark as needing aapt 596 */ 597 public static void markAaptRequested(IProject project) { 598 try { 599 String needsAapt = Boolean.TRUE.toString(); 600 project.setPersistentProperty(NEED_AAPT, needsAapt); 601 602 ProjectState state = Sdk.getProjectState(project); 603 if (state.isLibrary()) { 604 // For library projects also mark the dependent projects as needing full aapt 605 for (ProjectState parent : state.getFullParentProjects()) { 606 IProject parentProject = parent.getProject(); 607 // Mark the project, but only if it's open. Resource#setPersistentProperty 608 // only works on open projects. 609 if (parentProject.isOpen()) { 610 parentProject.setPersistentProperty(NEED_AAPT, needsAapt); 611 } 612 } 613 } 614 } catch (CoreException e) { 615 AdtPlugin.log(e, null); 616 } 617 } 618 619 /** 620 * Clear the "needs aapt" flag set by {@link #markAaptRequested(IProject)}. 621 * This is usually called when a project is built. Note that this will only 622 * clean the build flag on the given project, not on any downstream projects 623 * that depend on this project as a library project. 624 * 625 * @param project the project to clear from the needs aapt list 626 */ 627 public static void clearAaptRequest(IProject project) { 628 try { 629 project.setPersistentProperty(NEED_AAPT, null); 630 // Note that even if this project is a library project, we -don't- clear 631 // the aapt flags on the dependent projects since they may still depend 632 // on other dirty projects. When they are built, they will issue their 633 // own clear flag requests. 634 } catch (CoreException e) { 635 AdtPlugin.log(e, null); 636 } 637 } 638 639 /** 640 * Returns whether the given project needs a full aapt build. 641 * 642 * @param project the project to check 643 * @return true if the project needs a full aapt run 644 */ 645 public static boolean isAaptRequested(IProject project) { 646 try { 647 String b = project.getPersistentProperty(NEED_AAPT); 648 return b != null && Boolean.valueOf(b); 649 } catch (CoreException e) { 650 AdtPlugin.log(e, null); 651 } 652 653 return false; 654 } 655 } 656