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