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.common.rendering.LayoutLibrary; 21 import com.android.ide.common.sdk.LoadStatus; 22 import com.android.ide.eclipse.adt.AdtConstants; 23 import com.android.ide.eclipse.adt.AdtPlugin; 24 import com.android.ide.eclipse.adt.internal.build.DexWrapper; 25 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; 26 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 27 import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer; 28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; 29 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; 30 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; 31 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener; 32 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference; 33 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; 34 import com.android.io.StreamException; 35 import com.android.prefs.AndroidLocation.AndroidLocationException; 36 import com.android.sdklib.AndroidVersion; 37 import com.android.sdklib.IAndroidTarget; 38 import com.android.sdklib.ISdkLog; 39 import com.android.sdklib.SdkConstants; 40 import com.android.sdklib.SdkManager; 41 import com.android.sdklib.internal.avd.AvdManager; 42 import com.android.sdklib.internal.project.ProjectProperties; 43 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; 44 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 45 46 import org.eclipse.core.resources.IFile; 47 import org.eclipse.core.resources.IMarkerDelta; 48 import org.eclipse.core.resources.IProject; 49 import org.eclipse.core.resources.IResourceDelta; 50 import org.eclipse.core.resources.IncrementalProjectBuilder; 51 import org.eclipse.core.runtime.CoreException; 52 import org.eclipse.core.runtime.IPath; 53 import org.eclipse.core.runtime.IProgressMonitor; 54 import org.eclipse.core.runtime.IStatus; 55 import org.eclipse.core.runtime.Status; 56 import org.eclipse.core.runtime.jobs.Job; 57 import org.eclipse.jdt.core.IJavaProject; 58 import org.eclipse.jdt.core.JavaCore; 59 60 import java.io.File; 61 import java.io.IOException; 62 import java.net.MalformedURLException; 63 import java.net.URL; 64 import java.util.ArrayList; 65 import java.util.HashMap; 66 import java.util.HashSet; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Set; 70 import java.util.Map.Entry; 71 72 /** 73 * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used 74 * at the same time. 75 * 76 * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of 77 * the Sdk object. 78 * 79 * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. 80 */ 81 public final class Sdk { 82 private final static boolean DEBUG = false; 83 84 private final static Object LOCK = new Object(); 85 86 private static Sdk sCurrentSdk = null; 87 88 /** 89 * Map associating {@link IProject} and their state {@link ProjectState}. 90 * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}. 91 */ 92 private final static HashMap<IProject, ProjectState> sProjectStateMap = 93 new HashMap<IProject, ProjectState>(); 94 95 /** 96 * Data bundled using during the load of Target data. 97 * <p/>This contains the {@link LoadStatus} and a list of projects that attempted 98 * to compile before the loading was finished. Those projects will be recompiled 99 * at the end of the loading. 100 */ 101 private final static class TargetLoadBundle { 102 LoadStatus status; 103 final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>(); 104 } 105 106 private final SdkManager mManager; 107 private final DexWrapper mDexWrapper; 108 private final AvdManager mAvdManager; 109 110 /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */ 111 private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = 112 new HashMap<IAndroidTarget, AndroidTargetData>(); 113 /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */ 114 private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap = 115 new HashMap<IAndroidTarget, TargetLoadBundle>(); 116 117 /** 118 * If true the target data will never load anymore. The only way to reload them is to 119 * completely reload the SDK with {@link #loadSdk(String)} 120 */ 121 private boolean mDontLoadTargetData = false; 122 123 private final String mDocBaseUrl; 124 125 private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager(); 126 127 /** 128 * Classes implementing this interface will receive notification when targets are changed. 129 */ 130 public interface ITargetChangeListener { 131 /** 132 * Sent when project has its target changed. 133 */ 134 void onProjectTargetChange(IProject changedProject); 135 136 /** 137 * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, 138 * or the SDK is changed). 139 */ 140 void onTargetLoaded(IAndroidTarget target); 141 142 /** 143 * Called when the base content of the SDK is parsed. 144 */ 145 void onSdkLoaded(); 146 } 147 148 /** 149 * Basic abstract implementation of the ITargetChangeListener for the case where both 150 * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)} 151 * use the same code based on a simple test requiring to know the current IProject. 152 */ 153 public static abstract class TargetChangeListener implements ITargetChangeListener { 154 /** 155 * Returns the {@link IProject} associated with the listener. 156 */ 157 public abstract IProject getProject(); 158 159 /** 160 * Called when the listener needs to take action on the event. This is only called 161 * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project 162 * match the values received in {@link #onProjectTargetChange(IProject)} and 163 * {@link #onTargetLoaded(IAndroidTarget)}. 164 */ 165 public abstract void reload(); 166 167 public void onProjectTargetChange(IProject changedProject) { 168 if (changedProject != null && changedProject.equals(getProject())) { 169 reload(); 170 } 171 } 172 173 public void onTargetLoaded(IAndroidTarget target) { 174 IProject project = getProject(); 175 if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) { 176 reload(); 177 } 178 } 179 180 public void onSdkLoaded() { 181 // do nothing; 182 } 183 } 184 185 /** 186 * Returns the lock object used to synchronize all operations dealing with SDK, targets and 187 * projects. 188 */ 189 public static final Object getLock() { 190 return LOCK; 191 } 192 193 /** 194 * Loads an SDK and returns an {@link Sdk} object if success. 195 * <p/>If the SDK failed to load, it displays an error to the user. 196 * @param sdkLocation the OS path to the SDK. 197 */ 198 public static Sdk loadSdk(String sdkLocation) { 199 synchronized (LOCK) { 200 if (sCurrentSdk != null) { 201 sCurrentSdk.dispose(); 202 sCurrentSdk = null; 203 } 204 205 final ArrayList<String> logMessages = new ArrayList<String>(); 206 ISdkLog log = new ISdkLog() { 207 public void error(Throwable throwable, String errorFormat, Object... arg) { 208 if (errorFormat != null) { 209 logMessages.add(String.format("Error: " + errorFormat, arg)); 210 } 211 212 if (throwable != null) { 213 logMessages.add(throwable.getMessage()); 214 } 215 } 216 217 public void warning(String warningFormat, Object... arg) { 218 logMessages.add(String.format("Warning: " + warningFormat, arg)); 219 } 220 221 public void printf(String msgFormat, Object... arg) { 222 logMessages.add(String.format(msgFormat, arg)); 223 } 224 }; 225 226 // get an SdkManager object for the location 227 SdkManager manager = SdkManager.createManager(sdkLocation, log); 228 if (manager != null) { 229 // load DX. 230 DexWrapper dexWrapper = new DexWrapper(); 231 String dexLocation = 232 sdkLocation + File.separator + 233 SdkConstants.OS_SDK_PLATFORM_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR; 234 IStatus res = dexWrapper.loadDex(dexLocation); 235 if (res != Status.OK_STATUS) { 236 log.error(null, res.getMessage()); 237 dexWrapper = null; 238 } 239 240 // create the AVD Manager 241 AvdManager avdManager = null; 242 try { 243 avdManager = new AvdManager(manager, log); 244 } catch (AndroidLocationException e) { 245 log.error(e, "Error parsing the AVDs"); 246 } 247 sCurrentSdk = new Sdk(manager, dexWrapper, avdManager); 248 return sCurrentSdk; 249 } else { 250 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); 251 for (String msg : logMessages) { 252 sb.append('\n'); 253 sb.append(msg); 254 } 255 AdtPlugin.displayError("Android SDK", sb.toString()); 256 } 257 return null; 258 } 259 } 260 261 /** 262 * Returns the current {@link Sdk} object. 263 */ 264 public static Sdk getCurrent() { 265 synchronized (LOCK) { 266 return sCurrentSdk; 267 } 268 } 269 270 /** 271 * Returns the location (OS path) of the current SDK. 272 */ 273 public String getSdkLocation() { 274 return mManager.getLocation(); 275 } 276 277 /** 278 * Returns the URL to the local documentation. 279 * Can return null if no documentation is found in the current SDK. 280 * 281 * @return A file:// URL on the local documentation folder if it exists or null. 282 */ 283 public String getDocumentationBaseUrl() { 284 return mDocBaseUrl; 285 } 286 287 /** 288 * Returns the list of targets that are available in the SDK. 289 */ 290 public IAndroidTarget[] getTargets() { 291 return mManager.getTargets(); 292 } 293 294 /** 295 * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. 296 * 297 * @param hash the {@link IAndroidTarget} hash string. 298 * @return The matching {@link IAndroidTarget} or null. 299 */ 300 public IAndroidTarget getTargetFromHashString(String hash) { 301 return mManager.getTargetFromHashString(hash); 302 } 303 304 /** 305 * Initializes a new project with a target. This creates the <code>project.properties</code> 306 * file. 307 * @param project the project to intialize 308 * @param target the project's target. 309 * @throws IOException if creating the file failed in any way. 310 * @throws StreamException 311 */ 312 public void initProject(IProject project, IAndroidTarget target) 313 throws IOException, StreamException { 314 if (project == null || target == null) { 315 return; 316 } 317 318 synchronized (LOCK) { 319 // check if there's already a state? 320 ProjectState state = getProjectState(project); 321 322 ProjectPropertiesWorkingCopy properties = null; 323 324 if (state != null) { 325 properties = state.getProperties().makeWorkingCopy(); 326 } 327 328 if (properties == null) { 329 IPath location = project.getLocation(); 330 if (location == null) { // can return null when the project is being deleted. 331 // do nothing and return null; 332 return; 333 } 334 335 properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT); 336 } 337 338 // save the target hash string in the project persistent property 339 properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); 340 properties.save(); 341 } 342 } 343 344 /** 345 * Returns the {@link ProjectState} object associated with a given project. 346 * <p/> 347 * This method is the only way to properly get the project's {@link ProjectState} 348 * If the project has not yet been loaded, then it is loaded. 349 * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk} 350 * objects, and therefore is static. 351 * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects 352 * are replaced. 353 * @param project the request project 354 * @return the ProjectState for the project. 355 */ 356 @SuppressWarnings("deprecation") 357 public static ProjectState getProjectState(IProject project) { 358 if (project == null) { 359 return null; 360 } 361 362 synchronized (LOCK) { 363 ProjectState state = sProjectStateMap.get(project); 364 if (state == null) { 365 // load the project.properties from the project folder. 366 IPath location = project.getLocation(); 367 if (location == null) { // can return null when the project is being deleted. 368 // do nothing and return null; 369 return null; 370 } 371 372 String projectLocation = location.toOSString(); 373 374 ProjectProperties properties = ProjectProperties.load(projectLocation, 375 PropertyType.PROJECT); 376 if (properties == null) { 377 // legacy support: look for default.properties and rename it if needed. 378 properties = ProjectProperties.load(projectLocation, 379 PropertyType.LEGACY_DEFAULT); 380 381 if (properties == null) { 382 AdtPlugin.log(IStatus.ERROR, 383 "Failed to load properties file for project '%s'", 384 project.getName()); 385 return null; 386 } else { 387 //legacy mode. 388 // get a working copy with the new type "project" 389 ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy( 390 PropertyType.PROJECT); 391 // and save it 392 try { 393 wc.save(); 394 395 // delete the old file. 396 ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT); 397 } catch (Exception e) { 398 AdtPlugin.log(IStatus.ERROR, 399 "Failed to rename properties file to %1$s for project '%s2$'", 400 PropertyType.PROJECT.getFilename(), project.getName()); 401 } 402 } 403 } 404 405 state = new ProjectState(project, properties); 406 sProjectStateMap.put(project, state); 407 408 // try to resolve the target 409 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) { 410 sCurrentSdk.loadTarget(state); 411 } 412 } 413 414 return state; 415 } 416 } 417 418 /** 419 * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. 420 */ 421 public IAndroidTarget getTarget(IProject project) { 422 if (project == null) { 423 return null; 424 } 425 426 ProjectState state = getProjectState(project); 427 if (state != null) { 428 return state.getTarget(); 429 } 430 431 return null; 432 } 433 434 /** 435 * Loads the {@link IAndroidTarget} for a given project. 436 * <p/>This method will get the target hash string from the project properties, and resolve 437 * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}. 438 * @param state the state representing the project to load. 439 * @return the target that was loaded. 440 */ 441 public IAndroidTarget loadTarget(ProjectState state) { 442 IAndroidTarget target = null; 443 if (state != null) { 444 String hash = state.getTargetHashString(); 445 if (hash != null) { 446 state.setTarget(target = getTargetFromHashString(hash)); 447 } 448 } 449 450 return target; 451 } 452 453 /** 454 * Checks and loads (if needed) the data for a given target. 455 * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified 456 * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}. 457 * <p/>An optional project as second parameter can be given to be recompiled once the target 458 * data is finished loading. 459 * <p/>The return value is non-null only if the target data has already been loaded (and in this 460 * case is the status of the load operation) 461 * @param target the target to load. 462 * @param project an optional project to be recompiled when the target data is loaded. 463 * If the target is already loaded, nothing happens. 464 * @return The load status if the target data is already loaded. 465 */ 466 public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) { 467 boolean loadData = false; 468 469 synchronized (LOCK) { 470 if (mDontLoadTargetData) { 471 return LoadStatus.FAILED; 472 } 473 474 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 475 if (bundle == null) { 476 bundle = new TargetLoadBundle(); 477 mTargetDataStatusMap.put(target,bundle); 478 479 // set status to loading 480 bundle.status = LoadStatus.LOADING; 481 482 // add project to bundle 483 if (project != null) { 484 bundle.projecsToReload.add(project); 485 } 486 487 // and set the flag to start the loading below 488 loadData = true; 489 } else if (bundle.status == LoadStatus.LOADING) { 490 // add project to bundle 491 if (project != null) { 492 bundle.projecsToReload.add(project); 493 } 494 495 return bundle.status; 496 } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) { 497 return bundle.status; 498 } 499 } 500 501 if (loadData) { 502 Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) { 503 @Override 504 protected IStatus run(IProgressMonitor monitor) { 505 AdtPlugin plugin = AdtPlugin.getDefault(); 506 try { 507 IStatus status = new AndroidTargetParser(target).run(monitor); 508 509 IJavaProject[] javaProjectArray = null; 510 511 synchronized (LOCK) { 512 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 513 514 if (status.getCode() != IStatus.OK) { 515 bundle.status = LoadStatus.FAILED; 516 bundle.projecsToReload.clear(); 517 } else { 518 bundle.status = LoadStatus.LOADED; 519 520 // Prepare the array of project to recompile. 521 // The call is done outside of the synchronized block. 522 javaProjectArray = bundle.projecsToReload.toArray( 523 new IJavaProject[bundle.projecsToReload.size()]); 524 525 // and update the UI of the editors that depend on the target data. 526 plugin.updateTargetListeners(target); 527 } 528 } 529 530 if (javaProjectArray != null) { 531 AndroidClasspathContainerInitializer.updateProjects(javaProjectArray); 532 } 533 534 return status; 535 } catch (Throwable t) { 536 synchronized (LOCK) { 537 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 538 bundle.status = LoadStatus.FAILED; 539 } 540 541 AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$ 542 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 543 String.format( 544 "Parsing Data for %1$s failed", //$NON-NLS-1$ 545 target.hashString()), 546 t); 547 } 548 } 549 }; 550 job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs 551 job.schedule(); 552 } 553 554 // The only way to go through here is when the loading starts through the Job. 555 // Therefore the current status of the target is LOADING. 556 return LoadStatus.LOADING; 557 } 558 559 /** 560 * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. 561 */ 562 public AndroidTargetData getTargetData(IAndroidTarget target) { 563 synchronized (LOCK) { 564 return mTargetDataMap.get(target); 565 } 566 } 567 568 /** 569 * Return the {@link AndroidTargetData} for a given {@link IProject}. 570 */ 571 public AndroidTargetData getTargetData(IProject project) { 572 synchronized (LOCK) { 573 IAndroidTarget target = getTarget(project); 574 if (target != null) { 575 return getTargetData(target); 576 } 577 } 578 579 return null; 580 } 581 582 /** 583 * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not 584 * loaded properly, then this will return <code>null</code>. 585 */ 586 public DexWrapper getDexWrapper() { 587 return mDexWrapper; 588 } 589 590 /** 591 * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could 592 * be <code>null</code>. 593 */ 594 public AvdManager getAvdManager() { 595 return mAvdManager; 596 } 597 598 public static AndroidVersion getDeviceVersion(IDevice device) { 599 try { 600 Map<String, String> props = device.getProperties(); 601 String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL); 602 if (apiLevel == null) { 603 return null; 604 } 605 606 return new AndroidVersion(Integer.parseInt(apiLevel), 607 props.get((IDevice.PROP_BUILD_CODENAME))); 608 } catch (NumberFormatException e) { 609 return null; 610 } 611 } 612 613 public LayoutDeviceManager getLayoutDeviceManager() { 614 return mLayoutDeviceManager; 615 } 616 617 /** 618 * Returns a list of {@link ProjectState} representing projects depending, directly or 619 * indirectly on a given library project. 620 * @param project the library project. 621 * @return a possibly empty list of ProjectState. 622 */ 623 public static Set<ProjectState> getMainProjectsFor(IProject project) { 624 synchronized (LOCK) { 625 // first get the project directly depending on this. 626 HashSet<ProjectState> list = new HashSet<ProjectState>(); 627 628 // loop on all project and see if ProjectState.getLibrary returns a non null 629 // project. 630 for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) { 631 if (project != entry.getKey()) { 632 LibraryState library = entry.getValue().getLibrary(project); 633 if (library != null) { 634 list.add(entry.getValue()); 635 } 636 } 637 } 638 639 // now look for projects depending on the projects directly depending on the library. 640 HashSet<ProjectState> result = new HashSet<ProjectState>(list); 641 for (ProjectState p : list) { 642 if (p.isLibrary()) { 643 Set<ProjectState> set = getMainProjectsFor(p.getProject()); 644 result.addAll(set); 645 } 646 } 647 648 return result; 649 } 650 } 651 652 /** 653 * Unload the SDK's target data. 654 * 655 * If <var>preventReload</var>, this effect is final until the SDK instance is changed 656 * through {@link #loadSdk(String)}. 657 * 658 * The goal is to unload the targets to be able to replace existing targets with new ones, 659 * before calling {@link #loadSdk(String)} to fully reload the SDK. 660 * 661 * @param preventReload prevent the data from being loaded again for the remaining live of 662 * this {@link Sdk} instance. 663 */ 664 public void unloadTargetData(boolean preventReload) { 665 synchronized (LOCK) { 666 mDontLoadTargetData = preventReload; 667 668 // dispose of the target data. 669 for (AndroidTargetData data : mTargetDataMap.values()) { 670 data.dispose(); 671 } 672 673 mTargetDataMap.clear(); 674 } 675 } 676 677 private Sdk(SdkManager manager, DexWrapper dexWrapper, AvdManager avdManager) { 678 mManager = manager; 679 mDexWrapper = dexWrapper; 680 mAvdManager = avdManager; 681 682 // listen to projects closing 683 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); 684 // need to register the resource event listener first because the project listener 685 // is called back during registration with project opened in the workspace. 686 monitor.addResourceEventListener(mResourceEventListener); 687 monitor.addProjectListener(mProjectListener); 688 monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED); 689 690 // pre-compute some paths 691 mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + 692 SdkConstants.OS_SDK_DOCS_FOLDER); 693 694 // load the built-in and user layout devices 695 mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation()); 696 // and the ones from the add-on 697 loadLayoutDevices(); 698 699 // update whatever ProjectState is already present with new IAndroidTarget objects. 700 synchronized (LOCK) { 701 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { 702 entry.getValue().setTarget( 703 getTargetFromHashString(entry.getValue().getTargetHashString())); 704 } 705 } 706 } 707 708 /** 709 * Cleans and unloads the SDK. 710 */ 711 private void dispose() { 712 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); 713 monitor.removeProjectListener(mProjectListener); 714 monitor.removeFileListener(mFileListener); 715 monitor.removeResourceEventListener(mResourceEventListener); 716 717 // the IAndroidTarget objects are now obsolete so update the project states. 718 synchronized (LOCK) { 719 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { 720 entry.getValue().setTarget(null); 721 } 722 723 // dispose of the target data. 724 for (AndroidTargetData data : mTargetDataMap.values()) { 725 data.dispose(); 726 } 727 728 mTargetDataMap.clear(); 729 } 730 } 731 732 void setTargetData(IAndroidTarget target, AndroidTargetData data) { 733 synchronized (LOCK) { 734 mTargetDataMap.put(target, data); 735 } 736 } 737 738 /** 739 * Returns the URL to the local documentation. 740 * Can return null if no documentation is found in the current SDK. 741 * 742 * @param osDocsPath Path to the documentation folder in the current SDK. 743 * The folder may not actually exist. 744 * @return A file:// URL on the local documentation folder if it exists or null. 745 */ 746 private String getDocumentationBaseUrl(String osDocsPath) { 747 File f = new File(osDocsPath); 748 749 if (f.isDirectory()) { 750 try { 751 // Note: to create a file:// URL, one would typically use something like 752 // f.toURI().toURL().toString(). However this generates a broken path on 753 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of 754 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll 755 // do the correct thing manually. 756 757 String path = f.getAbsolutePath(); 758 if (File.separatorChar != '/') { 759 path = path.replace(File.separatorChar, '/'); 760 } 761 762 // For some reason the URL class doesn't add the mandatory "//" after 763 // the "file:" protocol name, so it has to be hacked into the path. 764 URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$ 765 String result = url.toString(); 766 return result; 767 } catch (MalformedURLException e) { 768 // ignore malformed URLs 769 } 770 } 771 772 return null; 773 } 774 775 /** 776 * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to 777 * load {@link LayoutDevice} from them. 778 */ 779 private void loadLayoutDevices() { 780 IAndroidTarget[] targets = mManager.getTargets(); 781 for (IAndroidTarget target : targets) { 782 if (target.isPlatform() == false) { 783 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML); 784 if (deviceXml.isFile()) { 785 mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml); 786 } 787 } 788 } 789 790 mLayoutDeviceManager.sealAddonLayoutDevices(); 791 } 792 793 /** 794 * Delegate listener for project changes. 795 */ 796 private IProjectListener mProjectListener = new IProjectListener() { 797 public void projectClosed(IProject project) { 798 onProjectRemoved(project, false /*deleted*/); 799 } 800 801 public void projectDeleted(IProject project) { 802 onProjectRemoved(project, true /*deleted*/); 803 } 804 805 private void onProjectRemoved(IProject removedProject, boolean deleted) { 806 try { 807 if (removedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 808 return; 809 } 810 } catch (CoreException e) { 811 // this can only happen if the project does not exist or is not open, neither 812 // of which can happen here since we're processing a Project removed/deleted event 813 // which is processed before the project is actually removed/closed. 814 } 815 816 if (DEBUG) { 817 System.out.println(">>> CLOSED: " + removedProject.getName()); 818 } 819 820 // get the target project 821 synchronized (LOCK) { 822 // Don't use getProject() as it could create the ProjectState if it's not 823 // there yet and this is not what we want. We want the current object. 824 // Therefore, direct access to the map. 825 ProjectState removedState = sProjectStateMap.get(removedProject); 826 if (removedState != null) { 827 // 1. clear the layout lib cache associated with this project 828 IAndroidTarget target = removedState.getTarget(); 829 if (target != null) { 830 // get the bridge for the target, and clear the cache for this project. 831 AndroidTargetData data = mTargetDataMap.get(target); 832 if (data != null) { 833 LayoutLibrary layoutLib = data.getLayoutLibrary(); 834 if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) { 835 layoutLib.clearCaches(removedProject); 836 } 837 } 838 } 839 840 // 2. if the project is a library, make sure to update the 841 // LibraryState for any project referencing it. 842 // Also, record the updated projects that are libraries, to update 843 // projects that depend on them. 844 for (ProjectState projectState : sProjectStateMap.values()) { 845 LibraryState libState = projectState.getLibrary(removedProject); 846 if (libState != null) { 847 // Close the library right away. 848 // This remove links between the LibraryState and the projectState. 849 // This is because in case of a rename of a project, projectClosed and 850 // projectOpened will be called before any other job is run, so we 851 // need to make sure projectOpened is closed with the main project 852 // state up to date. 853 libState.close(); 854 855 // record that this project changed, and in case it's a library 856 // that its parents need to be updated as well. 857 markProject(projectState, projectState.isLibrary()); 858 } 859 } 860 861 // now remove the project for the project map. 862 sProjectStateMap.remove(removedProject); 863 } 864 } 865 866 if (DEBUG) { 867 System.out.println("<<<"); 868 } 869 } 870 871 public void projectOpened(IProject project) { 872 onProjectOpened(project); 873 } 874 875 public void projectOpenedWithWorkspace(IProject project) { 876 // no need to force recompilation when projects are opened with the workspace. 877 onProjectOpened(project); 878 } 879 880 private void onProjectOpened(final IProject openedProject) { 881 try { 882 if (openedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 883 return; 884 } 885 } catch (CoreException e) { 886 // this can only happen if the project does not exist or is not open, neither 887 // of which can happen here since we're processing a Project opened event. 888 } 889 890 891 ProjectState openedState = getProjectState(openedProject); 892 if (openedState != null) { 893 if (DEBUG) { 894 System.out.println(">>> OPENED: " + openedProject.getName()); 895 } 896 897 synchronized (LOCK) { 898 final boolean isLibrary = openedState.isLibrary(); 899 final boolean hasLibraries = openedState.hasLibraries(); 900 901 if (isLibrary || hasLibraries) { 902 boolean foundLibraries = false; 903 // loop on all the existing project and update them based on this new 904 // project 905 for (ProjectState projectState : sProjectStateMap.values()) { 906 if (projectState != openedState) { 907 // If the project has libraries, check if this project 908 // is a reference. 909 if (hasLibraries) { 910 // ProjectState#needs() both checks if this is a missing library 911 // and updates LibraryState to contains the new values. 912 // This must always be called. 913 LibraryState libState = openedState.needs(projectState); 914 915 if (libState != null) { 916 // found a library! Add the main project to the list of 917 // modified project 918 foundLibraries = true; 919 } 920 } 921 922 // if the project is a library check if the other project depend 923 // on it. 924 if (isLibrary) { 925 // ProjectState#needs() both checks if this is a missing library 926 // and updates LibraryState to contains the new values. 927 // This must always be called. 928 LibraryState libState = projectState.needs(openedState); 929 930 if (libState != null) { 931 // There's a dependency! Add the project to the list of 932 // modified project, but also to a list of projects 933 // that saw one of its dependencies resolved. 934 markProject(projectState, projectState.isLibrary()); 935 } 936 } 937 } 938 } 939 940 // if the project has a libraries and we found at least one, we add 941 // the project to the list of modified project. 942 // Since we already went through the parent, no need to update them. 943 if (foundLibraries) { 944 markProject(openedState, false /*updateParents*/); 945 } 946 } 947 } 948 949 if (DEBUG) { 950 System.out.println("<<<"); 951 } 952 } 953 } 954 955 public void projectRenamed(IProject project, IPath from) { 956 // we don't actually care about this anymore. 957 } 958 }; 959 960 /** 961 * Delegate listener for file changes. 962 */ 963 private IFileListener mFileListener = new IFileListener() { 964 public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) { 965 if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) && 966 file.getParent() == file.getProject()) { 967 try { 968 // reload the content of the project.properties file and update 969 // the target. 970 IProject iProject = file.getProject(); 971 972 if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 973 return; 974 } 975 976 ProjectState state = Sdk.getProjectState(iProject); 977 978 // get the current target 979 IAndroidTarget oldTarget = state.getTarget(); 980 981 // get the current library flag 982 boolean wasLibrary = state.isLibrary(); 983 984 LibraryDifference diff = state.reloadProperties(); 985 986 // load the (possibly new) target. 987 IAndroidTarget newTarget = loadTarget(state); 988 989 // reload the libraries if needed 990 if (diff.hasDiff()) { 991 if (diff.added) { 992 synchronized (LOCK) { 993 for (ProjectState projectState : sProjectStateMap.values()) { 994 if (projectState != state) { 995 // need to call needs to do the libraryState link, 996 // but no need to look at the result, as we'll compare 997 // the result of getFullLibraryProjects() 998 // this is easier to due to indirect dependencies. 999 state.needs(projectState); 1000 } 1001 } 1002 } 1003 } 1004 1005 markProject(state, wasLibrary || state.isLibrary()); 1006 } 1007 1008 // apply the new target if needed. 1009 if (newTarget != oldTarget) { 1010 IJavaProject javaProject = BaseProjectHelper.getJavaProject( 1011 file.getProject()); 1012 if (javaProject != null) { 1013 AndroidClasspathContainerInitializer.updateProjects( 1014 new IJavaProject[] { javaProject }); 1015 } 1016 1017 // update the editors to reload with the new target 1018 AdtPlugin.getDefault().updateTargetListeners(iProject); 1019 } 1020 } catch (CoreException e) { 1021 // This can't happen as it's only for closed project (or non existing) 1022 // but in that case we can't get a fileChanged on this file. 1023 } 1024 } 1025 } 1026 }; 1027 1028 /** List of modified projects. This is filled in 1029 * {@link IProjectListener#projectOpened(IProject)}, 1030 * {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, 1031 * {@link IProjectListener#projectClosed(IProject)}, and 1032 * {@link IProjectListener#projectDeleted(IProject)} and processed in 1033 * {@link IResourceEventListener#resourceChangeEventEnd()}. 1034 */ 1035 private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>(); 1036 private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>(); 1037 1038 private void markProject(ProjectState projectState, boolean updateParents) { 1039 if (mModifiedProjects.contains(projectState) == false) { 1040 if (DEBUG) { 1041 System.out.println("\tMARKED: " + projectState.getProject().getName()); 1042 } 1043 mModifiedProjects.add(projectState); 1044 } 1045 1046 // if the project is resolved also add it to this list. 1047 if (updateParents) { 1048 if (mModifiedChildProjects.contains(projectState) == false) { 1049 if (DEBUG) { 1050 System.out.println("\tMARKED(child): " + projectState.getProject().getName()); 1051 } 1052 mModifiedChildProjects.add(projectState); 1053 } 1054 } 1055 } 1056 1057 /** 1058 * Delegate listener for resource changes. This is called before and after any calls to the 1059 * project and file listeners (for a given resource change event). 1060 */ 1061 private IResourceEventListener mResourceEventListener = new IResourceEventListener() { 1062 public void resourceChangeEventStart() { 1063 mModifiedProjects.clear(); 1064 mModifiedChildProjects.clear(); 1065 } 1066 1067 public void resourceChangeEventEnd() { 1068 if (mModifiedProjects.size() == 0) { 1069 return; 1070 } 1071 1072 // first make sure all the parents are updated 1073 updateParentProjects(); 1074 1075 // for all modified projects, update their library list 1076 // and gather their IProject 1077 final List<IJavaProject> projectList = new ArrayList<IJavaProject>(); 1078 for (ProjectState state : mModifiedProjects) { 1079 state.updateFullLibraryList(); 1080 projectList.add(JavaCore.create(state.getProject())); 1081 } 1082 1083 Job job = new Job("Android Library Update") { //$NON-NLS-1$ 1084 @Override 1085 protected IStatus run(IProgressMonitor monitor) { 1086 LibraryClasspathContainerInitializer.updateProjects( 1087 projectList.toArray(new IJavaProject[projectList.size()])); 1088 1089 for (IJavaProject javaProject : projectList) { 1090 try { 1091 javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, 1092 monitor); 1093 } catch (CoreException e) { 1094 // pass 1095 } 1096 } 1097 return Status.OK_STATUS; 1098 } 1099 }; 1100 job.setPriority(Job.BUILD); 1101 job.schedule(); 1102 } 1103 }; 1104 1105 /** 1106 * Updates all existing projects with a given list of new/updated libraries. 1107 * This loops through all opened projects and check if they depend on any of the given 1108 * library project, and if they do, they are linked together. 1109 * @param libraries the list of new/updated library projects. 1110 */ 1111 private void updateParentProjects() { 1112 if (mModifiedChildProjects.size() == 0) { 1113 return; 1114 } 1115 1116 ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects); 1117 mModifiedChildProjects.clear(); 1118 synchronized (LOCK) { 1119 // for each project for which we must update its parent, we loop on the parent 1120 // projects and adds them to the list of modified projects. If they are themselves 1121 // libraries, we add them too. 1122 for (ProjectState state : childProjects) { 1123 if (DEBUG) { 1124 System.out.println(">>> Updating parents of " + state.getProject().getName()); 1125 } 1126 List<ProjectState> parents = state.getParentProjects(); 1127 for (ProjectState parent : parents) { 1128 markProject(parent, parent.isLibrary()); 1129 } 1130 if (DEBUG) { 1131 System.out.println("<<<"); 1132 } 1133 } 1134 } 1135 1136 // done, but there may be parents that are also libraries. Need to update their parents. 1137 updateParentProjects(); 1138 } 1139 } 1140 1141