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.sdk; 18 19 import com.android.ddmlib.IDevice; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; 22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 23 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 24 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; 25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; 26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; 27 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener; 28 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge; 29 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference; 30 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; 31 import com.android.prefs.AndroidLocation.AndroidLocationException; 32 import com.android.sdklib.AndroidVersion; 33 import com.android.sdklib.IAndroidTarget; 34 import com.android.sdklib.ISdkLog; 35 import com.android.sdklib.SdkConstants; 36 import com.android.sdklib.SdkManager; 37 import com.android.sdklib.internal.avd.AvdManager; 38 import com.android.sdklib.internal.project.ProjectProperties; 39 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; 40 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 41 import com.android.sdklib.io.StreamException; 42 43 import org.eclipse.core.resources.IFile; 44 import org.eclipse.core.resources.IFolder; 45 import org.eclipse.core.resources.IMarkerDelta; 46 import org.eclipse.core.resources.IPathVariableManager; 47 import org.eclipse.core.resources.IProject; 48 import org.eclipse.core.resources.IProjectDescription; 49 import org.eclipse.core.resources.IResource; 50 import org.eclipse.core.resources.IResourceDelta; 51 import org.eclipse.core.resources.IWorkspaceRoot; 52 import org.eclipse.core.resources.IncrementalProjectBuilder; 53 import org.eclipse.core.resources.ResourcesPlugin; 54 import org.eclipse.core.runtime.CoreException; 55 import org.eclipse.core.runtime.IPath; 56 import org.eclipse.core.runtime.IProgressMonitor; 57 import org.eclipse.core.runtime.IStatus; 58 import org.eclipse.core.runtime.Path; 59 import org.eclipse.core.runtime.Status; 60 import org.eclipse.core.runtime.jobs.Job; 61 import org.eclipse.jdt.core.IClasspathEntry; 62 import org.eclipse.jdt.core.IJavaProject; 63 import org.eclipse.jdt.core.JavaCore; 64 65 import java.io.File; 66 import java.io.IOException; 67 import java.net.MalformedURLException; 68 import java.net.URL; 69 import java.util.ArrayList; 70 import java.util.Arrays; 71 import java.util.HashMap; 72 import java.util.HashSet; 73 import java.util.List; 74 import java.util.Map; 75 import java.util.Set; 76 import java.util.Map.Entry; 77 78 /** 79 * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used 80 * at the same time. 81 * 82 * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of 83 * the Sdk object. 84 * 85 * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. 86 */ 87 public final class Sdk { 88 private static final String PROP_LIBRARY = "_library"; //$NON-NLS-1$ 89 private static final String PROP_LIBRARY_NAME = "_library_name"; //$NON-NLS-1$ 90 public static final String CREATOR_ADT = "ADT"; //$NON-NLS-1$ 91 public static final String PROP_CREATOR = "_creator"; //$NON-NLS-1$ 92 private final static Object sLock = new Object(); 93 94 private static Sdk sCurrentSdk = null; 95 96 /** 97 * Map associating {@link IProject} and their state {@link ProjectState}. 98 * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}. 99 */ 100 private final static HashMap<IProject, ProjectState> sProjectStateMap = 101 new HashMap<IProject, ProjectState>(); 102 103 /** 104 * Data bundled using during the load of Target data. 105 * <p/>This contains the {@link LoadStatus} and a list of projects that attempted 106 * to compile before the loading was finished. Those projects will be recompiled 107 * at the end of the loading. 108 */ 109 private final static class TargetLoadBundle { 110 LoadStatus status; 111 final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>(); 112 } 113 114 private final SdkManager mManager; 115 private final AvdManager mAvdManager; 116 117 /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */ 118 private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = 119 new HashMap<IAndroidTarget, AndroidTargetData>(); 120 /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */ 121 private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap = 122 new HashMap<IAndroidTarget, TargetLoadBundle>(); 123 124 private final String mDocBaseUrl; 125 126 private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager(); 127 128 /** 129 * Classes implementing this interface will receive notification when targets are changed. 130 */ 131 public interface ITargetChangeListener { 132 /** 133 * Sent when project has its target changed. 134 */ 135 void onProjectTargetChange(IProject changedProject); 136 137 /** 138 * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, 139 * or the SDK is changed). 140 */ 141 void onTargetLoaded(IAndroidTarget target); 142 143 /** 144 * Called when the base content of the SDK is parsed. 145 */ 146 void onSdkLoaded(); 147 } 148 149 /** 150 * Basic abstract implementation of the ITargetChangeListener for the case where both 151 * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)} 152 * use the same code based on a simple test requiring to know the current IProject. 153 */ 154 public static abstract class TargetChangeListener implements ITargetChangeListener { 155 /** 156 * Returns the {@link IProject} associated with the listener. 157 */ 158 public abstract IProject getProject(); 159 160 /** 161 * Called when the listener needs to take action on the event. This is only called 162 * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project 163 * match the values received in {@link #onProjectTargetChange(IProject)} and 164 * {@link #onTargetLoaded(IAndroidTarget)}. 165 */ 166 public abstract void reload(); 167 168 public void onProjectTargetChange(IProject changedProject) { 169 if (changedProject != null && changedProject.equals(getProject())) { 170 reload(); 171 } 172 } 173 174 public void onTargetLoaded(IAndroidTarget target) { 175 IProject project = getProject(); 176 if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) { 177 reload(); 178 } 179 } 180 181 public void onSdkLoaded() { 182 // do nothing; 183 } 184 } 185 186 /** 187 * Returns the lock object used to synchronize all operations dealing with SDK, targets and 188 * projects. 189 */ 190 public static final Object getLock() { 191 return sLock; 192 } 193 194 /** 195 * Loads an SDK and returns an {@link Sdk} object if success. 196 * <p/>If the SDK failed to load, it displays an error to the user. 197 * @param sdkLocation the OS path to the SDK. 198 */ 199 public static Sdk loadSdk(String sdkLocation) { 200 synchronized (sLock) { 201 if (sCurrentSdk != null) { 202 sCurrentSdk.dispose(); 203 sCurrentSdk = null; 204 } 205 206 final ArrayList<String> logMessages = new ArrayList<String>(); 207 ISdkLog log = new ISdkLog() { 208 public void error(Throwable throwable, String errorFormat, Object... arg) { 209 if (errorFormat != null) { 210 logMessages.add(String.format("Error: " + errorFormat, arg)); 211 } 212 213 if (throwable != null) { 214 logMessages.add(throwable.getMessage()); 215 } 216 } 217 218 public void warning(String warningFormat, Object... arg) { 219 logMessages.add(String.format("Warning: " + warningFormat, arg)); 220 } 221 222 public void printf(String msgFormat, Object... arg) { 223 logMessages.add(String.format(msgFormat, arg)); 224 } 225 }; 226 227 // get an SdkManager object for the location 228 SdkManager manager = SdkManager.createManager(sdkLocation, log); 229 if (manager != null) { 230 AvdManager avdManager = null; 231 try { 232 avdManager = new AvdManager(manager, log); 233 } catch (AndroidLocationException e) { 234 log.error(e, "Error parsing the AVDs"); 235 } 236 sCurrentSdk = new Sdk(manager, avdManager); 237 return sCurrentSdk; 238 } else { 239 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); 240 for (String msg : logMessages) { 241 sb.append('\n'); 242 sb.append(msg); 243 } 244 AdtPlugin.displayError("Android SDK", sb.toString()); 245 } 246 return null; 247 } 248 } 249 250 /** 251 * Returns the current {@link Sdk} object. 252 */ 253 public static Sdk getCurrent() { 254 synchronized (sLock) { 255 return sCurrentSdk; 256 } 257 } 258 259 /** 260 * Returns the location (OS path) of the current SDK. 261 */ 262 public String getSdkLocation() { 263 return mManager.getLocation(); 264 } 265 266 /** 267 * Returns the URL to the local documentation. 268 * Can return null if no documentation is found in the current SDK. 269 * 270 * @return A file:// URL on the local documentation folder if it exists or null. 271 */ 272 public String getDocumentationBaseUrl() { 273 return mDocBaseUrl; 274 } 275 276 /** 277 * Returns the list of targets that are available in the SDK. 278 */ 279 public IAndroidTarget[] getTargets() { 280 return mManager.getTargets(); 281 } 282 283 /** 284 * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. 285 * 286 * @param hash the {@link IAndroidTarget} hash string. 287 * @return The matching {@link IAndroidTarget} or null. 288 */ 289 public IAndroidTarget getTargetFromHashString(String hash) { 290 return mManager.getTargetFromHashString(hash); 291 } 292 293 /** 294 * Initializes a new project with a target. This creates the <code>default.properties</code> 295 * file. 296 * @param project the project to intialize 297 * @param target the project's target. 298 * @throws IOException if creating the file failed in any way. 299 * @throws StreamException 300 */ 301 public void initProject(IProject project, IAndroidTarget target) 302 throws IOException, StreamException { 303 if (project == null || target == null) { 304 return; 305 } 306 307 synchronized (sLock) { 308 // check if there's already a state? 309 ProjectState state = getProjectState(project); 310 311 ProjectPropertiesWorkingCopy properties = null; 312 313 if (state != null) { 314 properties = state.getProperties().makeWorkingCopy(); 315 } 316 317 if (properties == null) { 318 IPath location = project.getLocation(); 319 if (location == null) { // can return null when the project is being deleted. 320 // do nothing and return null; 321 return; 322 } 323 324 properties = ProjectProperties.create(location.toOSString(), PropertyType.DEFAULT); 325 } 326 327 // save the target hash string in the project persistent property 328 properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); 329 properties.save(); 330 } 331 } 332 333 /** 334 * Returns the {@link ProjectState} object associated with a given project. 335 * <p/> 336 * This method is the only way to properly get the project's {@link ProjectState} 337 * If the project has not yet been loaded, then it is loaded. 338 * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk} 339 * objects, and therefore is static. 340 * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects 341 * are replaced. 342 * @param project the request project 343 * @return the ProjectState for the project. 344 */ 345 public static ProjectState getProjectState(IProject project) { 346 if (project == null) { 347 return null; 348 } 349 350 synchronized (sLock) { 351 ProjectState state = sProjectStateMap.get(project); 352 if (state == null) { 353 // load the default.properties from the project folder. 354 IPath location = project.getLocation(); 355 if (location == null) { // can return null when the project is being deleted. 356 // do nothing and return null; 357 return null; 358 } 359 360 ProjectProperties properties = ProjectProperties.load(location.toOSString(), 361 PropertyType.DEFAULT); 362 if (properties == null) { 363 AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'", 364 project.getName()); 365 return null; 366 } 367 368 state = new ProjectState(project, properties); 369 sProjectStateMap.put(project, state); 370 371 // try to resolve the target 372 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) { 373 sCurrentSdk.loadTarget(state); 374 } 375 } 376 377 return state; 378 } 379 } 380 381 /** 382 * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. 383 */ 384 public IAndroidTarget getTarget(IProject project) { 385 if (project == null) { 386 return null; 387 } 388 389 ProjectState state = getProjectState(project); 390 if (state != null) { 391 return state.getTarget(); 392 } 393 394 return null; 395 } 396 397 /** 398 * Loads the {@link IAndroidTarget} for a given project. 399 * <p/>This method will get the target hash string from the project properties, and resolve 400 * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}. 401 * @param state the state representing the project to load. 402 * @return the target that was loaded. 403 */ 404 public IAndroidTarget loadTarget(ProjectState state) { 405 IAndroidTarget target = null; 406 String hash = state.getTargetHashString(); 407 if (hash != null) { 408 state.setTarget(target = getTargetFromHashString(hash)); 409 } 410 411 return target; 412 } 413 414 /** 415 * Checks and loads (if needed) the data for a given target. 416 * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified 417 * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}. 418 * <p/>An optional project as second parameter can be given to be recompiled once the target 419 * data is finished loading. 420 * <p/>The return value is non-null only if the target data has already been loaded (and in this 421 * case is the status of the load operation) 422 * @param target the target to load. 423 * @param project an optional project to be recompiled when the target data is loaded. 424 * If the target is already loaded, nothing happens. 425 * @return The load status if the target data is already loaded. 426 */ 427 public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) { 428 boolean loadData = false; 429 430 synchronized (sLock) { 431 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 432 if (bundle == null) { 433 bundle = new TargetLoadBundle(); 434 mTargetDataStatusMap.put(target,bundle); 435 436 // set status to loading 437 bundle.status = LoadStatus.LOADING; 438 439 // add project to bundle 440 if (project != null) { 441 bundle.projecsToReload.add(project); 442 } 443 444 // and set the flag to start the loading below 445 loadData = true; 446 } else if (bundle.status == LoadStatus.LOADING) { 447 // add project to bundle 448 if (project != null) { 449 bundle.projecsToReload.add(project); 450 } 451 452 return bundle.status; 453 } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) { 454 return bundle.status; 455 } 456 } 457 458 if (loadData) { 459 Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) { 460 @Override 461 protected IStatus run(IProgressMonitor monitor) { 462 AdtPlugin plugin = AdtPlugin.getDefault(); 463 try { 464 IStatus status = new AndroidTargetParser(target).run(monitor); 465 466 IJavaProject[] javaProjectArray = null; 467 468 synchronized (sLock) { 469 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 470 471 if (status.getCode() != IStatus.OK) { 472 bundle.status = LoadStatus.FAILED; 473 bundle.projecsToReload.clear(); 474 } else { 475 bundle.status = LoadStatus.LOADED; 476 477 // Prepare the array of project to recompile. 478 // The call is done outside of the synchronized block. 479 javaProjectArray = bundle.projecsToReload.toArray( 480 new IJavaProject[bundle.projecsToReload.size()]); 481 482 // and update the UI of the editors that depend on the target data. 483 plugin.updateTargetListeners(target); 484 } 485 } 486 487 if (javaProjectArray != null) { 488 AndroidClasspathContainerInitializer.updateProjects(javaProjectArray); 489 } 490 491 return status; 492 } catch (Throwable t) { 493 synchronized (sLock) { 494 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 495 bundle.status = LoadStatus.FAILED; 496 } 497 498 AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$ 499 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 500 String.format( 501 "Parsing Data for %1$s failed", //$NON-NLS-1$ 502 target.hashString()), 503 t); 504 } 505 } 506 }; 507 job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs 508 job.schedule(); 509 } 510 511 // The only way to go through here is when the loading starts through the Job. 512 // Therefore the current status of the target is LOADING. 513 return LoadStatus.LOADING; 514 } 515 516 /** 517 * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. 518 */ 519 public AndroidTargetData getTargetData(IAndroidTarget target) { 520 synchronized (sLock) { 521 return mTargetDataMap.get(target); 522 } 523 } 524 525 /** 526 * Return the {@link AndroidTargetData} for a given {@link IProject}. 527 */ 528 public AndroidTargetData getTargetData(IProject project) { 529 synchronized (sLock) { 530 IAndroidTarget target = getTarget(project); 531 if (target != null) { 532 return getTargetData(target); 533 } 534 } 535 536 return null; 537 } 538 539 /** 540 * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could 541 * be <code>null</code>. 542 */ 543 public AvdManager getAvdManager() { 544 return mAvdManager; 545 } 546 547 public static AndroidVersion getDeviceVersion(IDevice device) { 548 try { 549 Map<String, String> props = device.getProperties(); 550 String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL); 551 if (apiLevel == null) { 552 return null; 553 } 554 555 return new AndroidVersion(Integer.parseInt(apiLevel), 556 props.get((IDevice.PROP_BUILD_CODENAME))); 557 } catch (NumberFormatException e) { 558 return null; 559 } 560 } 561 562 public LayoutDeviceManager getLayoutDeviceManager() { 563 return mLayoutDeviceManager; 564 } 565 566 /** 567 * Returns a list of {@link ProjectState} representing projects depending, directly or 568 * indirectly on a given library project. 569 * @param project the library project. 570 * @return a possibly empty list of ProjectState. 571 */ 572 public static Set<ProjectState> getMainProjectsFor(IProject project) { 573 synchronized (sLock) { 574 // first get the project directly depending on this. 575 HashSet<ProjectState> list = new HashSet<ProjectState>(); 576 577 // loop on all project and see if ProjectState.getLibrary returns a non null 578 // project. 579 for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) { 580 if (project != entry.getKey()) { 581 LibraryState library = entry.getValue().getLibrary(project); 582 if (library != null) { 583 list.add(entry.getValue()); 584 } 585 } 586 } 587 588 // now look for projects depending on the projects directly depending on the library. 589 HashSet<ProjectState> result = new HashSet<ProjectState>(list); 590 for (ProjectState p : list) { 591 if (p.isLibrary()) { 592 Set<ProjectState> set = getMainProjectsFor(p.getProject()); 593 result.addAll(set); 594 } 595 } 596 597 return result; 598 } 599 } 600 601 private Sdk(SdkManager manager, AvdManager avdManager) { 602 mManager = manager; 603 mAvdManager = avdManager; 604 605 // listen to projects closing 606 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); 607 monitor.addProjectListener(mProjectListener); 608 monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED); 609 monitor.addResourceEventListener(mResourceEventListener); 610 611 // pre-compute some paths 612 mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + 613 SdkConstants.OS_SDK_DOCS_FOLDER); 614 615 // load the built-in and user layout devices 616 mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation()); 617 // and the ones from the add-on 618 loadLayoutDevices(); 619 620 // update whatever ProjectState is already present with new IAndroidTarget objects. 621 synchronized (sLock) { 622 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { 623 entry.getValue().setTarget( 624 getTargetFromHashString(entry.getValue().getTargetHashString())); 625 } 626 } 627 } 628 629 /** 630 * Cleans and unloads the SDK. 631 */ 632 private void dispose() { 633 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); 634 monitor.removeProjectListener(mProjectListener); 635 monitor.removeFileListener(mFileListener); 636 monitor.removeResourceEventListener(mResourceEventListener); 637 638 // the IAndroidTarget objects are now obsolete so update the project states. 639 synchronized (sLock) { 640 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { 641 entry.getValue().setTarget(null); 642 } 643 } 644 } 645 646 void setTargetData(IAndroidTarget target, AndroidTargetData data) { 647 synchronized (sLock) { 648 mTargetDataMap.put(target, data); 649 } 650 } 651 652 /** 653 * Returns the URL to the local documentation. 654 * Can return null if no documentation is found in the current SDK. 655 * 656 * @param osDocsPath Path to the documentation folder in the current SDK. 657 * The folder may not actually exist. 658 * @return A file:// URL on the local documentation folder if it exists or null. 659 */ 660 private String getDocumentationBaseUrl(String osDocsPath) { 661 File f = new File(osDocsPath); 662 663 if (f.isDirectory()) { 664 try { 665 // Note: to create a file:// URL, one would typically use something like 666 // f.toURI().toURL().toString(). However this generates a broken path on 667 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of 668 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll 669 // do the correct thing manually. 670 671 String path = f.getAbsolutePath(); 672 if (File.separatorChar != '/') { 673 path = path.replace(File.separatorChar, '/'); 674 } 675 676 // For some reason the URL class doesn't add the mandatory "//" after 677 // the "file:" protocol name, so it has to be hacked into the path. 678 URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$ 679 String result = url.toString(); 680 return result; 681 } catch (MalformedURLException e) { 682 // ignore malformed URLs 683 } 684 } 685 686 return null; 687 } 688 689 /** 690 * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to 691 * load {@link LayoutDevice} from them. 692 */ 693 private void loadLayoutDevices() { 694 IAndroidTarget[] targets = mManager.getTargets(); 695 for (IAndroidTarget target : targets) { 696 if (target.isPlatform() == false) { 697 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML); 698 if (deviceXml.isFile()) { 699 mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml); 700 } 701 } 702 } 703 704 mLayoutDeviceManager.sealAddonLayoutDevices(); 705 } 706 707 /** 708 * Delegate listener for project changes. 709 */ 710 private IProjectListener mProjectListener = new IProjectListener() { 711 public void projectClosed(IProject project) { 712 onProjectRemoved(project, false /*deleted*/); 713 } 714 715 public void projectDeleted(IProject project) { 716 onProjectRemoved(project, true /*deleted*/); 717 } 718 719 private void onProjectRemoved(IProject project, boolean deleted) { 720 // get the target project 721 synchronized (sLock) { 722 // Don't use getProject() as it could create the ProjectState if it's not 723 // there yet and this is not what we want. We want the current object. 724 // Therefore, direct access to the map. 725 ProjectState state = sProjectStateMap.get(project); 726 if (state != null) { 727 // 1. clear the layout lib cache associated with this project 728 IAndroidTarget target = state.getTarget(); 729 if (target != null) { 730 // get the bridge for the target, and clear the cache for this project. 731 AndroidTargetData data = mTargetDataMap.get(target); 732 if (data != null) { 733 LayoutBridge bridge = data.getLayoutBridge(); 734 if (bridge != null && bridge.status == LoadStatus.LOADED) { 735 bridge.bridge.clearCaches(project); 736 } 737 } 738 } 739 740 // 2. if the project is a library, make sure to update the 741 // LibraryState for any main project using this. 742 // Also, record the updated projects that are libraries, to update 743 // projects that depend on them. 744 ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>(); 745 for (ProjectState projectState : sProjectStateMap.values()) { 746 LibraryState libState = projectState.getLibrary(project); 747 if (libState != null) { 748 // get the current libraries. 749 IProject[] oldLibraries = projectState.getFullLibraryProjects(); 750 751 // the unlink below will work in the job, but we need to close 752 // the library right away. 753 // This is because in case of a rename of a project, projectClosed and 754 // projectOpened will be called before any other job is run, so we 755 // need to make sure projectOpened is closed with the main project 756 // state up to date. 757 libState.close(); 758 759 760 // edit the project to remove the linked source folder. 761 // this also calls LibraryState.close(); 762 LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries); 763 if (bundle != null) { 764 queueLinkUpdateBundle(bundle); 765 } 766 767 if (projectState.isLibrary()) { 768 updatedLibraries.add(projectState); 769 } 770 } 771 } 772 773 if (deleted) { 774 // remove the linked path variable 775 disposeLibraryProject(project); 776 } 777 778 // now remove the project for the project map. 779 sProjectStateMap.remove(project); 780 781 // update the projects that depend on the updated project 782 updateProjectsWithNewLibraries(updatedLibraries); 783 } 784 } 785 } 786 787 public void projectOpened(IProject project) { 788 onProjectOpened(project); 789 } 790 791 public void projectOpenedWithWorkspace(IProject project) { 792 // no need to force recompilation when projects are opened with the workspace. 793 onProjectOpened(project); 794 } 795 796 private void onProjectOpened(final IProject openedProject) { 797 ProjectState openedState = getProjectState(openedProject); 798 if (openedState != null) { 799 if (openedState.hasLibraries()) { 800 // list of library to link to the opened project. 801 final ArrayList<IProject> libsToLink = new ArrayList<IProject>(); 802 803 // Look for all other opened projects to see if any is a library for the opened 804 // project. 805 synchronized (sLock) { 806 for (ProjectState projectState : sProjectStateMap.values()) { 807 if (projectState != openedState) { 808 // ProjectState#needs() both checks if this is a missing library 809 // and updates LibraryState to contains the new values. 810 LibraryState libState = openedState.needs(projectState); 811 812 if (libState != null) { 813 // we have a match! Add the library to the list (if it was 814 // not added through an indirect dependency before). 815 IProject libProject = libState.getProjectState().getProject(); 816 if (libsToLink.contains(libProject) == false) { 817 libsToLink.add(libProject); 818 } 819 820 // now find what this depends on, and add it too. 821 // The order here doesn't matter 822 // as it's just to add the linked source folder, so there's no 823 // need to use ProjectState#getFullLibraryProjects() which 824 // could return project that have already been added anyway. 825 fillProjectDependenciesList(libState.getProjectState(), 826 libsToLink); 827 } 828 } 829 } 830 } 831 832 // create a link bundle always, because even if there's no libraries to add 833 // to the CPE, the cleaning of invalid CPE must happen. 834 LinkUpdateBundle bundle = new LinkUpdateBundle(); 835 bundle.mProject = openedProject; 836 bundle.mNewLibraryProjects = libsToLink.toArray( 837 new IProject[libsToLink.size()]); 838 bundle.mCleanupCPE = true; 839 queueLinkUpdateBundle(bundle); 840 } 841 842 // if the project is a library, then add it to the list of projects being opened. 843 // They will be processed in IResourceEventListener#resourceChangeEventEnd. 844 // This is done so that we are sure to process all the projects being opened 845 // first and only then process projects depending on the projects that were opened. 846 if (openedState.isLibrary()) { 847 setupLibraryProject(openedProject); 848 849 mOpenedLibraryProjects.add(openedState); 850 } 851 } 852 } 853 854 public void projectRenamed(IProject project, IPath from) { 855 System.out.println("RENAMED: " + project); 856 // a project was renamed. 857 // if the project is a library, look for any project that depended on it 858 // and update it. (default.properties and linked source folder) 859 ProjectState renamedState = getProjectState(project); 860 if (renamedState.isLibrary()) { 861 // remove the variable 862 disposeLibraryProject(from.lastSegment()); 863 864 // update the project depending on the library 865 synchronized (sLock) { 866 for (ProjectState projectState : sProjectStateMap.values()) { 867 if (projectState != renamedState && projectState.isMissingLibraries()) { 868 IPath oldRelativePath = makeRelativeTo(from, 869 projectState.getProject().getFullPath()); 870 871 IPath newRelativePath = makeRelativeTo(project.getFullPath(), 872 projectState.getProject().getFullPath()); 873 874 // get the current libraries 875 IProject[] oldLibraries = projectState.getFullLibraryProjects(); 876 877 // update the library for the main project. 878 LibraryState libState = projectState.updateLibrary( 879 oldRelativePath.toString(), newRelativePath.toString(), 880 renamedState); 881 if (libState != null) { 882 // this project depended on the renamed library, create a bundle 883 // with the whole library difference (in case the renamed library 884 // also depends on libraries). 885 886 LinkUpdateBundle bundle = getLinkBundle(projectState, 887 oldLibraries); 888 queueLinkUpdateBundle(bundle); 889 890 // add it to the opened projects to update whatever depends 891 // on it 892 if (projectState.isLibrary()) { 893 mOpenedLibraryProjects.add(projectState); 894 } 895 } 896 } 897 } 898 } 899 } 900 } 901 }; 902 903 /** 904 * Delegate listener for file changes. 905 */ 906 private IFileListener mFileListener = new IFileListener() { 907 public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) { 908 if (SdkConstants.FN_DEFAULT_PROPERTIES.equals(file.getName()) && 909 file.getParent() == file.getProject()) { 910 try { 911 // reload the content of the default.properties file and update 912 // the target. 913 IProject iProject = file.getProject(); 914 ProjectState state = Sdk.getProjectState(iProject); 915 916 // get the current target 917 IAndroidTarget oldTarget = state.getTarget(); 918 919 // get the current library flag 920 boolean wasLibrary = state.isLibrary(); 921 922 // get the current list of project dependencies 923 IProject[] oldLibraries = state.getFullLibraryProjects(); 924 925 LibraryDifference diff = state.reloadProperties(); 926 927 // load the (possibly new) target. 928 IAndroidTarget newTarget = loadTarget(state); 929 930 // check if this is a new library 931 if (state.isLibrary() && wasLibrary == false) { 932 setupLibraryProject(iProject); 933 } 934 935 // reload the libraries if needed 936 if (diff.hasDiff()) { 937 if (diff.added) { 938 synchronized (sLock) { 939 for (ProjectState projectState : sProjectStateMap.values()) { 940 if (projectState != state) { 941 // need to call needs to do the libraryState link, 942 // but no need to look at the result, as we'll compare 943 // the result of getFullLibraryProjects() 944 // this is easier to due to indirect dependencies. 945 state.needs(projectState); 946 } 947 } 948 } 949 } 950 951 // and build the real difference. A list of new projects and a list of 952 // removed project. 953 // This is not the same as the added/removed libraries because libraries 954 // could be indirect dependencies through several different direct 955 // dependencies so it's easier to compare the full lists before and after 956 // the reload. 957 LinkUpdateBundle bundle = getLinkBundle(state, oldLibraries); 958 if (bundle != null) { 959 queueLinkUpdateBundle(bundle); 960 } 961 } 962 963 // apply the new target if needed. 964 if (newTarget != oldTarget) { 965 IJavaProject javaProject = BaseProjectHelper.getJavaProject( 966 file.getProject()); 967 if (javaProject != null) { 968 AndroidClasspathContainerInitializer.updateProjects( 969 new IJavaProject[] { javaProject }); 970 } 971 972 // update the editors to reload with the new target 973 AdtPlugin.getDefault().updateTargetListeners(iProject); 974 } 975 } catch (CoreException e) { 976 // This can't happen as it's only for closed project (or non existing) 977 // but in that case we can't get a fileChanged on this file. 978 } 979 } 980 } 981 }; 982 983 /** List of opened project. This is filled in {@link IProjectListener#projectOpened(IProject)} 984 * and {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, and processed in 985 * {@link IResourceEventListener#resourceChangeEventEnd()}. 986 */ 987 private final ArrayList<ProjectState> mOpenedLibraryProjects = new ArrayList<ProjectState>(); 988 989 /** 990 * Delegate listener for resource changes. This is called before and after any calls to the 991 * project and file listeners (for a given resource change event). 992 */ 993 private IResourceEventListener mResourceEventListener = new IResourceEventListener() { 994 public void resourceChangeEventStart() { 995 // pass 996 } 997 998 public void resourceChangeEventEnd() { 999 updateProjectsWithNewLibraries(mOpenedLibraryProjects); 1000 mOpenedLibraryProjects.clear(); 1001 } 1002 }; 1003 1004 /** 1005 * Action bundle to update library links on a project. 1006 * 1007 * @see Sdk#queueLinkUpdateBundle(LinkUpdateBundle) 1008 * @see Sdk#updateLibraryLinks(LinkUpdateBundle, IProgressMonitor) 1009 */ 1010 private static class LinkUpdateBundle { 1011 1012 /** The main project receiving the library links. */ 1013 IProject mProject = null; 1014 /** A list (possibly null/empty) of projects that should be linked. */ 1015 IProject[] mNewLibraryProjects = null; 1016 /** an optional old library path that needs to be removed at the same time as the new 1017 * libraries are added. Can be <code>null</code> in which case no libraries are removed. */ 1018 IPath mDeletedLibraryPath = null; 1019 /** A list (possibly null/empty) of projects that should be unlinked */ 1020 IProject[] mRemovedLibraryProjects = null; 1021 /** Whether unknown IClasspathEntry (that were flagged as being added by ADT) are to be 1022 * removed. This is typically only set to <code>true</code> when the project is opened. */ 1023 boolean mCleanupCPE = false; 1024 1025 @Override 1026 public String toString() { 1027 return String.format( 1028 "LinkUpdateBundle: %1$s (clean: %2$s) > added: %3$s, removed: %4$s, deleted: %5$s", //$NON-NLS-1$ 1029 mProject.getName(), 1030 mCleanupCPE, 1031 Arrays.toString(mNewLibraryProjects), 1032 Arrays.toString(mRemovedLibraryProjects), 1033 mDeletedLibraryPath); 1034 } 1035 } 1036 1037 private final ArrayList<LinkUpdateBundle> mLinkActionBundleQueue = 1038 new ArrayList<LinkUpdateBundle>(); 1039 1040 /** 1041 * Queues a {@link LinkUpdateBundle} bundle to be run by a job. 1042 * 1043 * All action bundles are executed in a job in the exact order they are added. 1044 * This is convenient when several actions must be executed in a job consecutively (instead 1045 * of in parallel as it would happen if each started its own job) but it is impossible 1046 * to manually control the job that's running them (for instance each action is started from 1047 * different callbacks such as {@link IProjectListener#projectOpened(IProject)}. 1048 * 1049 * If the job is not yet started, or has terminated due to lack of action bundle, it is 1050 * restarted. 1051 * 1052 * @param bundle the action bundle to execute 1053 */ 1054 private void queueLinkUpdateBundle(LinkUpdateBundle bundle) { 1055 boolean startJob = false; 1056 synchronized (mLinkActionBundleQueue) { 1057 startJob = mLinkActionBundleQueue.size() == 0; 1058 mLinkActionBundleQueue.add(bundle); 1059 } 1060 1061 if (startJob) { 1062 Job job = new Job("Android Library Update") { //$NON-NLS-1$ 1063 @Override 1064 protected IStatus run(IProgressMonitor monitor) { 1065 // loop until there's no bundle to process 1066 while (true) { 1067 // get the bundle, but don't remove until we're done, or a new job could be 1068 // started. 1069 LinkUpdateBundle bundle = null; 1070 synchronized (mLinkActionBundleQueue) { 1071 // there is always a bundle at this point, as they are only removed 1072 // at the end of this method, and the job is only started after adding 1073 // one 1074 bundle = mLinkActionBundleQueue.get(0); 1075 } 1076 1077 // process the bundle. 1078 try { 1079 updateLibraryLinks(bundle, monitor); 1080 } catch (Exception e) { 1081 AdtPlugin.log(e, "Failed to process bundle: %1$s", //$NON-NLS-1$ 1082 bundle.toString()); 1083 } 1084 1085 try { 1086 // force a recompile 1087 bundle.mProject.build(IncrementalProjectBuilder.FULL_BUILD, monitor); 1088 } catch (Exception e) { 1089 // no need to log those. 1090 } 1091 1092 // remove it from the list. 1093 synchronized (mLinkActionBundleQueue) { 1094 mLinkActionBundleQueue.remove(0); 1095 1096 // no more bundle to process? done. 1097 if (mLinkActionBundleQueue.size() == 0) { 1098 return Status.OK_STATUS; 1099 } 1100 } 1101 } 1102 } 1103 }; 1104 job.setPriority(Job.BUILD); 1105 job.schedule(); 1106 } 1107 } 1108 1109 1110 /** 1111 * Adds to a list the resolved {@link IProject} dependencies for a given {@link ProjectState}. 1112 * This recursively goes down to indirect dependencies. 1113 * 1114 * <strong>The list is filled in an order that is not valid for calling <code>aapt</code> 1115 * </strong>. 1116 * Use {@link ProjectState#getFullLibraryProjects()} for use with <code>aapt</code>. 1117 * 1118 * @param projectState the ProjectState of the project from which to add the libraries. 1119 * @param libraries the list of {@link IProject} to fill. 1120 */ 1121 private void fillProjectDependenciesList(ProjectState projectState, 1122 ArrayList<IProject> libraries) { 1123 for (LibraryState libState : projectState.getLibraries()) { 1124 ProjectState libProjectState = libState.getProjectState(); 1125 1126 // only care if the LibraryState has a resolved ProjectState 1127 if (libProjectState != null) { 1128 // try not to add duplicate. This can happen if a project depends on 2 different 1129 // libraries that both depend on the same one. 1130 IProject libProject = libProjectState.getProject(); 1131 if (libraries.contains(libProject) == false) { 1132 libraries.add(libProject); 1133 } 1134 1135 // process the libraries of this library too. 1136 fillProjectDependenciesList(libProjectState, libraries); 1137 } 1138 } 1139 } 1140 1141 /** 1142 * Sets up a path variable for a given project. 1143 * The name of the variable is based on the name of the project. However some valid character 1144 * for project names can be invalid for variable paths. 1145 * {@link #getLibraryVariableName(String)} return the name of the variable based on the 1146 * project name. 1147 * 1148 * @param libProject the project 1149 * 1150 * @see IPathVariableManager 1151 * @see #getLibraryVariableName(String) 1152 */ 1153 private void setupLibraryProject(IProject libProject) { 1154 // if needed add a path var for this library 1155 IPathVariableManager pathVarMgr = 1156 ResourcesPlugin.getWorkspace().getPathVariableManager(); 1157 IPath libPath = libProject.getLocation(); 1158 1159 final String varName = getLibraryVariableName(libProject.getName()); 1160 1161 if (libPath.equals(pathVarMgr.getValue(varName)) == false) { 1162 try { 1163 pathVarMgr.setValue(varName, libPath); 1164 } catch (CoreException e) { 1165 AdtPlugin.logAndPrintError(e, "Library Project", 1166 "Unable to set linked path var '%1$s' for library %2$s: %3$s", //$NON-NLS-1$ 1167 varName, libPath.toOSString(), e.getMessage()); 1168 } 1169 } 1170 } 1171 1172 1173 /** 1174 * Deletes the path variable that was setup for the given project. 1175 * @param project the project 1176 * @see #disposeLibraryProject(String) 1177 */ 1178 private void disposeLibraryProject(IProject project) { 1179 disposeLibraryProject(project.getName()); 1180 } 1181 1182 /** 1183 * Deletes the path variable that was setup for the given project name. 1184 * The name of the variable is based on the name of the project. However some valid character 1185 * for project names can be invalid for variable paths. 1186 * {@link #getLibraryVariableName(String)} return the name of the variable based on the 1187 * project name. 1188 * @param projectName the name of the project, unmodified. 1189 */ 1190 private void disposeLibraryProject(String projectName) { 1191 IPathVariableManager pathVarMgr = 1192 ResourcesPlugin.getWorkspace().getPathVariableManager(); 1193 1194 final String varName = getLibraryVariableName(projectName); 1195 1196 // remove the value by setting the value to null. 1197 try { 1198 pathVarMgr.setValue(varName, null /*path*/); 1199 } catch (CoreException e) { 1200 String message = String.format("Unable to remove linked path var '%1$s'", //$NON-NLS-1$ 1201 varName); 1202 AdtPlugin.log(e, message); 1203 } 1204 } 1205 1206 /** 1207 * Returns a valid path variable name based on the name of a library project. 1208 * @param name the name of the library project. 1209 */ 1210 private String getLibraryVariableName(String name) { 1211 return "_android_" + name.replaceAll("-", "_"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 1212 } 1213 1214 /** 1215 * Update the library links for a project 1216 * 1217 * This does the follow: 1218 * - add/remove the library projects to the main projects dynamic reference list. This is used 1219 * by the builders to receive resource change deltas for library projects and figure out what 1220 * needs to be recompiled/recreated. 1221 * - create new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each 1222 * source folder for each new library project. 1223 * - remove the {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each 1224 * source folder for each removed library project. 1225 * - If {@link LinkUpdateBundle#mCleanupCPE} is set to true, all CPE created by ADT that cannot 1226 * be resolved are removed. This should only be used when the project is opened. 1227 * 1228 * <strong>This must not be called directly. Instead the {@link LinkUpdateBundle} must 1229 * be run through a job with {@link #queueLinkUpdateBundle(LinkUpdateBundle)}.</strong> 1230 * 1231 * @param bundle The {@link LinkUpdateBundle} action bundle that contains all the parameters 1232 * necessary to execute the action. 1233 * @param monitor an {@link IProgressMonitor}. 1234 * @return an {@link IStatus} with the status of the action. 1235 */ 1236 private IStatus updateLibraryLinks(LinkUpdateBundle bundle, IProgressMonitor monitor) { 1237 if (bundle.mProject.isOpen() == false) { 1238 return Status.OK_STATUS; 1239 } 1240 try { 1241 // add the library to the list of dynamic references. This is necessary to receive 1242 // notifications that the library content changed in the builders. 1243 IProjectDescription projectDescription = bundle.mProject.getDescription(); 1244 IProject[] refs = projectDescription.getDynamicReferences(); 1245 1246 if (refs.length > 0) { 1247 ArrayList<IProject> list = new ArrayList<IProject>(Arrays.asList(refs)); 1248 1249 // remove a previous library if needed (in case of a rename) 1250 if (bundle.mDeletedLibraryPath != null) { 1251 // since project basically have only one segment that matter, 1252 // just check the names 1253 removeFromList(list, bundle.mDeletedLibraryPath.lastSegment()); 1254 } 1255 1256 if (bundle.mRemovedLibraryProjects != null) { 1257 for (IProject removedProject : bundle.mRemovedLibraryProjects) { 1258 removeFromList(list, removedProject.getName()); 1259 } 1260 } 1261 1262 // add the new ones if they don't exist 1263 if (bundle.mNewLibraryProjects != null) { 1264 for (IProject newProject : bundle.mNewLibraryProjects) { 1265 if (list.contains(newProject) == false) { 1266 list.add(newProject); 1267 } 1268 } 1269 } 1270 1271 // set the changed list 1272 projectDescription.setDynamicReferences( 1273 list.toArray(new IProject[list.size()])); 1274 } else { 1275 if (bundle.mNewLibraryProjects != null) { 1276 projectDescription.setDynamicReferences(bundle.mNewLibraryProjects); 1277 } 1278 } 1279 1280 // get the current classpath entries for the project to add the new source 1281 // folders. 1282 IJavaProject javaProject = JavaCore.create(bundle.mProject); 1283 IClasspathEntry[] entries = javaProject.getRawClasspath(); 1284 ArrayList<IClasspathEntry> classpathEntries = new ArrayList<IClasspathEntry>( 1285 Arrays.asList(entries)); 1286 1287 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 1288 1289 // loop on the classpath entries and look for CPE_SOURCE entries that 1290 // are linked folders, then record them for comparison later as we add the new 1291 // ones. 1292 ArrayList<IClasspathEntry> cpeToRemove = new ArrayList<IClasspathEntry>(); 1293 for (IClasspathEntry classpathEntry : classpathEntries) { 1294 if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { 1295 IPath path = classpathEntry.getPath(); 1296 IResource linkedRes = wsRoot.findMember(path); 1297 if (linkedRes != null && linkedRes.isLinked() && 1298 CREATOR_ADT.equals(ProjectHelper.loadStringProperty( 1299 linkedRes, PROP_CREATOR))) { 1300 1301 // add always to list if we're doing clean-up 1302 if (bundle.mCleanupCPE) { 1303 cpeToRemove.add(classpathEntry); 1304 } else { 1305 String libName = ProjectHelper.loadStringProperty(linkedRes, 1306 PROP_LIBRARY_NAME); 1307 if (libName != null && isRemovedLibrary(bundle, libName)) { 1308 cpeToRemove.add(classpathEntry); 1309 } 1310 } 1311 } 1312 } 1313 } 1314 1315 // loop on the projects to add. 1316 if (bundle.mNewLibraryProjects != null) { 1317 for (IProject library : bundle.mNewLibraryProjects) { 1318 if (library.isOpen() == false) { 1319 continue; 1320 } 1321 final String libName = library.getName(); 1322 final String varName = getLibraryVariableName(libName); 1323 1324 // get the list of source folders for the library. 1325 ArrayList<IPath> sourceFolderPaths = BaseProjectHelper.getSourceClasspaths( 1326 library); 1327 1328 // loop on all the source folder, ignoring FD_GEN and add them 1329 // as linked folder 1330 for (IPath sourceFolderPath : sourceFolderPaths) { 1331 IResource sourceFolder = wsRoot.findMember(sourceFolderPath); 1332 if (sourceFolder == null || sourceFolder.isLinked()) { 1333 continue; 1334 } 1335 1336 IPath relativePath = sourceFolder.getProjectRelativePath(); 1337 if (SdkConstants.FD_GEN_SOURCES.equals(relativePath.toString())) { 1338 continue; 1339 } 1340 1341 // create the linked path 1342 IPath linkedPath = new Path(varName).append(relativePath); 1343 1344 // look for an existing CPE that has the same linked path and that was 1345 // going to be removed. 1346 IClasspathEntry match = findClasspathEntryMatch(cpeToRemove, linkedPath, 1347 null); 1348 1349 if (match == null) { 1350 // no match, create one 1351 // get a string version, to make up the linked folder name 1352 String srcFolderName = relativePath.toString().replace( 1353 "/", //$NON-NLS-1$ 1354 "_"); //$NON-NLS-1$ 1355 1356 // folder name 1357 String folderName = libName + "_" + srcFolderName; //$NON-NLS-1$ 1358 1359 // create a linked resource for the library using the path var. 1360 IFolder libSrc = bundle.mProject.getFolder(folderName); 1361 IPath libSrcPath = libSrc.getFullPath(); 1362 1363 // check if there's a CPE that would conflict, in which case it needs to 1364 // be removed (this can happen for existing CPE that don't match an open 1365 // project) 1366 match = findClasspathEntryMatch(classpathEntries, null/*rawPath*/, 1367 libSrcPath); 1368 if (match != null) { 1369 classpathEntries.remove(match); 1370 } 1371 1372 // the path of the linked resource is based on the path variable 1373 // representing the library project, followed by the source folder name. 1374 libSrc.createLink(linkedPath, IResource.REPLACE, monitor); 1375 1376 // set some persistent properties on it to know that it was 1377 // created by ADT. 1378 ProjectHelper.saveStringProperty(libSrc, PROP_CREATOR, CREATOR_ADT); 1379 ProjectHelper.saveResourceProperty(libSrc, PROP_LIBRARY, library); 1380 ProjectHelper.saveStringProperty(libSrc, PROP_LIBRARY_NAME, 1381 library.getName()); 1382 1383 // add the source folder to the classpath entries 1384 classpathEntries.add(JavaCore.newSourceEntry(libSrcPath)); 1385 } else { 1386 // there's a valid match, do nothing, but remove the match from 1387 // the list of previously existing CPE. 1388 cpeToRemove.remove(match); 1389 } 1390 } 1391 } 1392 } 1393 1394 // remove the CPE that should be removed. 1395 classpathEntries.removeAll(cpeToRemove); 1396 1397 // set the new list 1398 javaProject.setRawClasspath( 1399 classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]), 1400 monitor); 1401 1402 // and delete the folders of the CPE that were removed (must be done after) 1403 for (IClasspathEntry cpe : cpeToRemove) { 1404 IResource res = wsRoot.findMember(cpe.getPath()); 1405 res.delete(true, monitor); 1406 } 1407 1408 return Status.OK_STATUS; 1409 } catch (CoreException e) { 1410 AdtPlugin.logAndPrintError(e, bundle.mProject.getName(), 1411 "Failed to create library links: %1$s", //$NON-NLS-1$ 1412 e.getMessage()); 1413 return e.getStatus(); 1414 } 1415 } 1416 1417 private boolean isRemovedLibrary(LinkUpdateBundle bundle, String libName) { 1418 if (bundle.mDeletedLibraryPath != null && 1419 libName.equals(bundle.mDeletedLibraryPath.lastSegment())) { 1420 return true; 1421 } 1422 1423 if (bundle.mRemovedLibraryProjects != null) { 1424 for (IProject removedProject : bundle.mRemovedLibraryProjects) { 1425 if (libName.equals(removedProject.getName())) { 1426 return true; 1427 } 1428 } 1429 } 1430 1431 return false; 1432 } 1433 1434 /** 1435 * Computes the library difference based on a previous list and a current state, and creates 1436 * a {@link LinkUpdateBundle} action to update the given project. 1437 * @param project The current project state 1438 * @param oldLibraries the list of old libraries. Typically the result of 1439 * {@link ProjectState#getFullLibraryProjects()} before the ProjectState is updated. 1440 * @return null if there no action to take, or a {@link LinkUpdateBundle} object to run. 1441 */ 1442 private LinkUpdateBundle getLinkBundle(ProjectState project, IProject[] oldLibraries) { 1443 // get the new full list of projects 1444 IProject[] newLibraries = project.getFullLibraryProjects(); 1445 1446 // and build the real difference. A list of new projects and a list of 1447 // removed project. 1448 // This is not the same as the added/removed libraries because libraries 1449 // could be indirect dependencies through several different direct 1450 // dependencies so it's easier to compare the full lists before and after 1451 // the reload. 1452 1453 List<IProject> addedLibs = new ArrayList<IProject>(); 1454 List<IProject> removedLibs = new ArrayList<IProject>(); 1455 1456 // first get the list of new projects. 1457 for (IProject newLibrary : newLibraries) { 1458 boolean found = false; 1459 for (IProject oldLibrary : oldLibraries) { 1460 if (newLibrary.equals(oldLibrary)) { 1461 found = true; 1462 break; 1463 } 1464 } 1465 1466 // if it was not found in the old libraries, it's really new 1467 if (found == false) { 1468 addedLibs.add(newLibrary); 1469 } 1470 } 1471 1472 // now the list of removed projects. 1473 for (IProject oldLibrary : oldLibraries) { 1474 boolean found = false; 1475 for (IProject newLibrary : newLibraries) { 1476 if (newLibrary.equals(oldLibrary)) { 1477 found = true; 1478 break; 1479 } 1480 } 1481 1482 // if it was not found in the new libraries, it's really been removed 1483 if (found == false) { 1484 removedLibs.add(oldLibrary); 1485 } 1486 } 1487 1488 if (addedLibs.size() > 0 || removedLibs.size() > 0) { 1489 LinkUpdateBundle bundle = new LinkUpdateBundle(); 1490 bundle.mProject = project.getProject(); 1491 bundle.mNewLibraryProjects = 1492 addedLibs.toArray(new IProject[addedLibs.size()]); 1493 bundle.mRemovedLibraryProjects = 1494 removedLibs.toArray(new IProject[removedLibs.size()]); 1495 return bundle; 1496 } 1497 1498 return null; 1499 } 1500 1501 /** 1502 * Removes a project from a list based on its name. 1503 * @param projects the list of projects. 1504 * @param name the name of the project to remove. 1505 */ 1506 private void removeFromList(List<IProject> projects, String name) { 1507 final int count = projects.size(); 1508 for (int i = 0 ; i < count ; i++) { 1509 // since project basically have only one segment that matter, 1510 // just check the names 1511 if (projects.get(i).getName().equals(name)) { 1512 projects.remove(i); 1513 return; 1514 } 1515 } 1516 } 1517 1518 /** 1519 * Returns a {@link IClasspathEntry} from the given list whose linked path match the given path. 1520 * @param cpeList a list of {@link IClasspathEntry} of {@link IClasspathEntry#getEntryKind()} 1521 * {@link IClasspathEntry#CPE_SOURCE} whose {@link IClasspathEntry#getPath()} 1522 * points to a linked folder. 1523 * @param rawPath the raw path to compare to. Can be null if <var>path</var> is used instead. 1524 * @param path the path to compare to. Can be null if <var>rawPath</var> is used instead. 1525 * @return the matching IClasspathEntry or null. 1526 */ 1527 private IClasspathEntry findClasspathEntryMatch(ArrayList<IClasspathEntry> cpeList, 1528 IPath rawPath, IPath path) { 1529 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 1530 for (IClasspathEntry cpe : cpeList) { 1531 IPath cpePath = cpe.getPath(); 1532 // test the normal path of the resource. 1533 if (path != null && path.equals(cpePath)) { 1534 return cpe; 1535 } 1536 1537 IResource res = wsRoot.findMember(cpePath); 1538 // getRawLocation returns the path that the linked folder points to. 1539 if (rawPath != null && res.getRawLocation().equals(rawPath)) { 1540 return cpe; 1541 } 1542 1543 } 1544 return null; 1545 } 1546 1547 /** 1548 * Updates all existing projects with a given list of new/updated libraries. 1549 * This loops through all opened projects and check if they depend on any of the given 1550 * library project, and if they do, they are linked together. 1551 * @param libraries the list of new/updated library projects. 1552 */ 1553 private void updateProjectsWithNewLibraries(List<ProjectState> libraries) { 1554 if (libraries.size() == 0) { 1555 return; 1556 } 1557 1558 ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>(); 1559 synchronized (sLock) { 1560 // for each projects, look for projects that depend on it, and update them. 1561 // Once they are updated (meaning ProjectState#needs() has been called on them), 1562 // we add them to the list so that can be updated as well. 1563 for (ProjectState projectState : sProjectStateMap.values()) { 1564 // record the current library dependencies 1565 IProject[] oldLibraries = projectState.getFullLibraryProjects(); 1566 1567 boolean needLibraryDependenciesUpdated = false; 1568 for (ProjectState library : libraries) { 1569 // Normally we would only need to test if ProjectState#needs returns non null, 1570 // meaning the link between the project and the library has not been 1571 // done yet. 1572 // However what matters here is that the library is a dependency, 1573 // period. If the library project was updated, then we redo the link, 1574 // with all indirect dependencies (which *have* changed, since this is 1575 // what this method is all about.) 1576 // We still need to call ProjectState#needs to make the link in case it's not 1577 // been done yet (which can happen if the library project was just opened). 1578 if (projectState != library) { 1579 // call needs in case this new library was just opened, and the link needs 1580 // to be done 1581 LibraryState libState = projectState.needs(library); 1582 if (libState == null && projectState.dependsOn(library)) { 1583 // ProjectState.needs only returns true if the library was needed. 1584 // but we also need to check the case where the project depends on 1585 // the library but the link was already done. 1586 needLibraryDependenciesUpdated = true; 1587 } 1588 } 1589 } 1590 1591 if (needLibraryDependenciesUpdated) { 1592 projectState.updateFullLibraryList(); 1593 } 1594 1595 LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries); 1596 if (bundle != null) { 1597 queueLinkUpdateBundle(bundle); 1598 1599 // if this updated project is a library, add it to the list, so that 1600 // projects depending on it get updated too. 1601 if (projectState.isLibrary() && 1602 updatedLibraries.contains(projectState) == false) { 1603 updatedLibraries.add(projectState); 1604 } 1605 } 1606 } 1607 } 1608 1609 // done, but there may be updated projects that were libraries, so we need to do the same 1610 // for this libraries, to update the project there were depending on. 1611 updateProjectsWithNewLibraries(updatedLibraries); 1612 } 1613 1614 /** 1615 * Computes a new IPath targeting a given target, but relative to a given base. 1616 * <p/>{@link IPath#makeRelativeTo(IPath, IPath)} is only available in 3.5 and later. 1617 * <p/>This is based on the implementation {@link Path#makeRelativeTo(IPath)}. 1618 * @param target the target of the IPath 1619 * @param base the IPath to base the relative path on. 1620 * @return the relative IPath 1621 */ 1622 public static IPath makeRelativeTo(IPath target, IPath base) { 1623 //can't make relative if devices are not equal 1624 if (target.getDevice() != base.getDevice() && (target.getDevice() == null || 1625 !target.getDevice().equalsIgnoreCase(base.getDevice()))) 1626 return target; 1627 int commonLength = target.matchingFirstSegments(base); 1628 final int differenceLength = base.segmentCount() - commonLength; 1629 final int newSegmentLength = differenceLength + target.segmentCount() - commonLength; 1630 if (newSegmentLength == 0) 1631 return Path.EMPTY; 1632 String[] newSegments = new String[newSegmentLength]; 1633 //add parent references for each segment different from the base 1634 Arrays.fill(newSegments, 0, differenceLength, ".."); //$NON-NLS-1$ 1635 //append the segments of this path not in common with the base 1636 System.arraycopy(target.segments(), commonLength, newSegments, 1637 differenceLength, newSegmentLength - differenceLength); 1638 1639 StringBuilder sb = new StringBuilder(); 1640 for (String s : newSegments) { 1641 sb.append(s).append('/'); 1642 } 1643 1644 return new Path(null, sb.toString()); 1645 } 1646 } 1647 1648