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