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 static com.android.SdkConstants.DOT_XML; 20 import static com.android.SdkConstants.EXT_JAR; 21 import static com.android.SdkConstants.FD_RES; 22 23 import com.android.SdkConstants; 24 import com.android.annotations.NonNull; 25 import com.android.annotations.Nullable; 26 import com.android.ddmlib.IDevice; 27 import com.android.ide.common.rendering.LayoutLibrary; 28 import com.android.ide.common.sdk.LoadStatus; 29 import com.android.ide.eclipse.adt.AdtConstants; 30 import com.android.ide.eclipse.adt.AdtPlugin; 31 import com.android.ide.eclipse.adt.internal.build.DexWrapper; 32 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; 33 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 34 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 35 import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer; 36 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 37 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; 38 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; 39 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; 40 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener; 41 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference; 42 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; 43 import com.android.io.StreamException; 44 import com.android.prefs.AndroidLocation.AndroidLocationException; 45 import com.android.sdklib.AndroidVersion; 46 import com.android.sdklib.BuildToolInfo; 47 import com.android.sdklib.IAndroidTarget; 48 import com.android.sdklib.SdkManager; 49 import com.android.sdklib.devices.DeviceManager; 50 import com.android.sdklib.internal.avd.AvdManager; 51 import com.android.sdklib.internal.project.ProjectProperties; 52 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 53 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; 54 import com.android.sdklib.repository.FullRevision; 55 import com.android.utils.ILogger; 56 import com.google.common.collect.Maps; 57 58 import org.eclipse.core.resources.IFile; 59 import org.eclipse.core.resources.IFolder; 60 import org.eclipse.core.resources.IMarker; 61 import org.eclipse.core.resources.IMarkerDelta; 62 import org.eclipse.core.resources.IProject; 63 import org.eclipse.core.resources.IResource; 64 import org.eclipse.core.resources.IResourceDelta; 65 import org.eclipse.core.resources.IncrementalProjectBuilder; 66 import org.eclipse.core.resources.ResourcesPlugin; 67 import org.eclipse.core.runtime.CoreException; 68 import org.eclipse.core.runtime.IPath; 69 import org.eclipse.core.runtime.IProgressMonitor; 70 import org.eclipse.core.runtime.IStatus; 71 import org.eclipse.core.runtime.QualifiedName; 72 import org.eclipse.core.runtime.Status; 73 import org.eclipse.core.runtime.jobs.Job; 74 import org.eclipse.jdt.core.IJavaProject; 75 import org.eclipse.jdt.core.JavaCore; 76 import org.eclipse.jdt.core.JavaModelException; 77 import org.eclipse.jface.preference.IPreferenceStore; 78 import org.eclipse.ui.IEditorDescriptor; 79 import org.eclipse.ui.IEditorInput; 80 import org.eclipse.ui.IEditorPart; 81 import org.eclipse.ui.IEditorReference; 82 import org.eclipse.ui.IFileEditorInput; 83 import org.eclipse.ui.IWorkbenchPage; 84 import org.eclipse.ui.IWorkbenchPartSite; 85 import org.eclipse.ui.IWorkbenchWindow; 86 import org.eclipse.ui.PartInitException; 87 import org.eclipse.ui.PlatformUI; 88 import org.eclipse.ui.ide.IDE; 89 90 import java.io.File; 91 import java.io.IOException; 92 import java.net.MalformedURLException; 93 import java.net.URL; 94 import java.util.ArrayList; 95 import java.util.Arrays; 96 import java.util.Collection; 97 import java.util.HashMap; 98 import java.util.HashSet; 99 import java.util.List; 100 import java.util.Map; 101 import java.util.Map.Entry; 102 import java.util.Set; 103 import java.util.concurrent.atomic.AtomicBoolean; 104 105 /** 106 * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used 107 * at the same time. 108 * 109 * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of 110 * the Sdk object. 111 * 112 * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. 113 */ 114 public final class Sdk { 115 private final static boolean DEBUG = false; 116 117 private final static Object LOCK = new Object(); 118 119 private static Sdk sCurrentSdk = null; 120 121 /** 122 * Map associating {@link IProject} and their state {@link ProjectState}. 123 * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}. 124 */ 125 private final static HashMap<IProject, ProjectState> sProjectStateMap = 126 new HashMap<IProject, ProjectState>(); 127 128 /** 129 * Data bundled using during the load of Target data. 130 * <p/>This contains the {@link LoadStatus} and a list of projects that attempted 131 * to compile before the loading was finished. Those projects will be recompiled 132 * at the end of the loading. 133 */ 134 private final static class TargetLoadBundle { 135 LoadStatus status; 136 final HashSet<IJavaProject> projectsToReload = new HashSet<IJavaProject>(); 137 } 138 139 private final SdkManager mManager; 140 private final Map<String, DexWrapper> mDexWrappers = Maps.newHashMap(); 141 private final AvdManager mAvdManager; 142 private final DeviceManager mDeviceManager; 143 144 /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */ 145 private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = 146 new HashMap<IAndroidTarget, AndroidTargetData>(); 147 /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */ 148 private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap = 149 new HashMap<IAndroidTarget, TargetLoadBundle>(); 150 151 /** 152 * If true the target data will never load anymore. The only way to reload them is to 153 * completely reload the SDK with {@link #loadSdk(String)} 154 */ 155 private boolean mDontLoadTargetData = false; 156 157 private final String mDocBaseUrl; 158 159 /** 160 * Classes implementing this interface will receive notification when targets are changed. 161 */ 162 public interface ITargetChangeListener { 163 /** 164 * Sent when project has its target changed. 165 */ onProjectTargetChange(IProject changedProject)166 void onProjectTargetChange(IProject changedProject); 167 168 /** 169 * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, 170 * or the SDK is changed). 171 */ onTargetLoaded(IAndroidTarget target)172 void onTargetLoaded(IAndroidTarget target); 173 174 /** 175 * Called when the base content of the SDK is parsed. 176 */ onSdkLoaded()177 void onSdkLoaded(); 178 } 179 180 /** 181 * Basic abstract implementation of the ITargetChangeListener for the case where both 182 * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)} 183 * use the same code based on a simple test requiring to know the current IProject. 184 */ 185 public static abstract class TargetChangeListener implements ITargetChangeListener { 186 /** 187 * Returns the {@link IProject} associated with the listener. 188 */ getProject()189 public abstract IProject getProject(); 190 191 /** 192 * Called when the listener needs to take action on the event. This is only called 193 * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project 194 * match the values received in {@link #onProjectTargetChange(IProject)} and 195 * {@link #onTargetLoaded(IAndroidTarget)}. 196 */ reload()197 public abstract void reload(); 198 199 @Override onProjectTargetChange(IProject changedProject)200 public void onProjectTargetChange(IProject changedProject) { 201 if (changedProject != null && changedProject.equals(getProject())) { 202 reload(); 203 } 204 } 205 206 @Override onTargetLoaded(IAndroidTarget target)207 public void onTargetLoaded(IAndroidTarget target) { 208 IProject project = getProject(); 209 if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) { 210 reload(); 211 } 212 } 213 214 @Override onSdkLoaded()215 public void onSdkLoaded() { 216 // do nothing; 217 } 218 } 219 220 /** 221 * Returns the lock object used to synchronize all operations dealing with SDK, targets and 222 * projects. 223 */ 224 @NonNull getLock()225 public static final Object getLock() { 226 return LOCK; 227 } 228 229 /** 230 * Loads an SDK and returns an {@link Sdk} object if success. 231 * <p/>If the SDK failed to load, it displays an error to the user. 232 * @param sdkLocation the OS path to the SDK. 233 */ 234 @Nullable loadSdk(String sdkLocation)235 public static Sdk loadSdk(String sdkLocation) { 236 synchronized (LOCK) { 237 if (sCurrentSdk != null) { 238 sCurrentSdk.dispose(); 239 sCurrentSdk = null; 240 } 241 242 final AtomicBoolean hasWarning = new AtomicBoolean(); 243 final AtomicBoolean hasError = new AtomicBoolean(); 244 final ArrayList<String> logMessages = new ArrayList<String>(); 245 ILogger log = new ILogger() { 246 @Override 247 public void error(@Nullable Throwable throwable, @Nullable String errorFormat, 248 Object... arg) { 249 hasError.set(true); 250 if (errorFormat != null) { 251 logMessages.add(String.format("Error: " + errorFormat, arg)); 252 } 253 254 if (throwable != null) { 255 logMessages.add(throwable.getMessage()); 256 } 257 } 258 259 @Override 260 public void warning(@NonNull String warningFormat, Object... arg) { 261 hasWarning.set(true); 262 logMessages.add(String.format("Warning: " + warningFormat, arg)); 263 } 264 265 @Override 266 public void info(@NonNull String msgFormat, Object... arg) { 267 logMessages.add(String.format(msgFormat, arg)); 268 } 269 270 @Override 271 public void verbose(@NonNull String msgFormat, Object... arg) { 272 info(msgFormat, arg); 273 } 274 }; 275 276 // get an SdkManager object for the location 277 SdkManager manager = SdkManager.createManager(sdkLocation, log); 278 try { 279 if (manager == null) { 280 hasError.set(true); 281 } else { 282 // create the AVD Manager 283 AvdManager avdManager = null; 284 try { 285 avdManager = AvdManager.getInstance(manager.getLocalSdk(), log); 286 } catch (AndroidLocationException e) { 287 log.error(e, "Error parsing the AVDs"); 288 } 289 sCurrentSdk = new Sdk(manager, avdManager); 290 return sCurrentSdk; 291 } 292 } finally { 293 if (hasError.get() || hasWarning.get()) { 294 StringBuilder sb = new StringBuilder( 295 String.format("%s when loading the SDK:\n", 296 hasError.get() ? "Error" : "Warning")); 297 for (String msg : logMessages) { 298 sb.append('\n'); 299 sb.append(msg); 300 } 301 if (hasError.get()) { 302 AdtPlugin.printErrorToConsole("Android SDK", sb.toString()); 303 AdtPlugin.displayError("Android SDK", sb.toString()); 304 } else { 305 AdtPlugin.printToConsole("Android SDK", sb.toString()); 306 } 307 } 308 } 309 return null; 310 } 311 } 312 313 /** 314 * Returns the current {@link Sdk} object. 315 */ 316 @Nullable getCurrent()317 public static Sdk getCurrent() { 318 synchronized (LOCK) { 319 return sCurrentSdk; 320 } 321 } 322 323 /** 324 * Returns the location of the current SDK as an OS path string. 325 * Guaranteed to be terminated by a platform-specific path separator. 326 * <p/> 327 * Due to {@link File} canonicalization, this MAY differ from the string used to initialize 328 * the SDK path. 329 * 330 * @return The SDK OS path or null if no SDK is setup. 331 * @deprecated Consider using {@link #getSdkFileLocation()} instead. 332 * @see #getSdkFileLocation() 333 */ 334 @Deprecated 335 @Nullable getSdkOsLocation()336 public String getSdkOsLocation() { 337 String path = mManager == null ? null : mManager.getLocation(); 338 if (path != null) { 339 // For backward compatibility make sure it ends with a separator. 340 // This used to be the case when the SDK Manager was created from a String path 341 // but now that a File is internally used the trailing dir separator is lost. 342 if (path.length() > 0 && !path.endsWith(File.separator)) { 343 path = path + File.separator; 344 } 345 } 346 return path; 347 } 348 349 /** 350 * Returns the location of the current SDK as a {@link File} or null. 351 * 352 * @return The SDK OS path or null if no SDK is setup. 353 */ 354 @Nullable getSdkFileLocation()355 public File getSdkFileLocation() { 356 if (mManager == null || mManager.getLocalSdk() == null) { 357 return null; 358 } 359 return mManager.getLocalSdk().getLocation(); 360 } 361 362 /** 363 * Returns a <em>new</em> {@link SdkManager} that can parse the SDK located 364 * at the current {@link #getSdkOsLocation()}. 365 * <p/> 366 * Implementation detail: The {@link Sdk} has its own internal manager with 367 * a custom logger which is not designed to be useful for outsiders. Callers 368 * who need their own {@link SdkManager} for parsing will often want to control 369 * the logger for their own need. 370 * <p/> 371 * This is just a convenient method equivalent to writing: 372 * <pre>SdkManager.createManager(Sdk.getCurrent().getSdkLocation(), log);</pre> 373 * 374 * @param log The logger for the {@link SdkManager}. 375 * @return A new {@link SdkManager} parsing the same location. 376 */ getNewSdkManager(@onNull ILogger log)377 public @Nullable SdkManager getNewSdkManager(@NonNull ILogger log) { 378 return SdkManager.createManager(getSdkOsLocation(), log); 379 } 380 381 /** 382 * Returns the URL to the local documentation. 383 * Can return null if no documentation is found in the current SDK. 384 * 385 * @return A file:// URL on the local documentation folder if it exists or null. 386 */ 387 @Nullable getDocumentationBaseUrl()388 public String getDocumentationBaseUrl() { 389 return mDocBaseUrl; 390 } 391 392 /** 393 * Returns the list of targets that are available in the SDK. 394 */ getTargets()395 public IAndroidTarget[] getTargets() { 396 return mManager.getTargets(); 397 } 398 399 /** 400 * Queries the underlying SDK Manager to check whether the platforms or addons 401 * directories have changed on-disk. Does not reload the SDK. 402 * <p/> 403 * This is a quick test based on the presence of the directories, their timestamps 404 * and a quick checksum of the source.properties files. It's possible to have 405 * false positives (e.g. if a file is manually modified in a platform) or false 406 * negatives (e.g. if a platform data file is changed manually in a 2nd level 407 * directory without altering the source.properties.) 408 */ haveTargetsChanged()409 public boolean haveTargetsChanged() { 410 return mManager.hasChanged(); 411 } 412 413 /** 414 * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. 415 * 416 * @param hash the {@link IAndroidTarget} hash string. 417 * @return The matching {@link IAndroidTarget} or null. 418 */ 419 @Nullable getTargetFromHashString(@onNull String hash)420 public IAndroidTarget getTargetFromHashString(@NonNull String hash) { 421 return mManager.getTargetFromHashString(hash); 422 } 423 424 @Nullable getBuildToolInfo(@ullable String buildToolVersion)425 public BuildToolInfo getBuildToolInfo(@Nullable String buildToolVersion) { 426 if (buildToolVersion != null) { 427 try { 428 return mManager.getBuildTool(FullRevision.parseRevision(buildToolVersion)); 429 } catch (Exception e) { 430 // ignore, return null below. 431 } 432 } 433 434 return null; 435 } 436 437 @Nullable getLatestBuildTool()438 public BuildToolInfo getLatestBuildTool() { 439 return mManager.getLatestBuildTool(); 440 } 441 442 /** 443 * Initializes a new project with a target. This creates the <code>project.properties</code> 444 * file. 445 * @param project the project to initialize 446 * @param target the project's target. 447 * @throws IOException if creating the file failed in any way. 448 * @throws StreamException if processing the project property file fails 449 */ initProject(@ullable IProject project, @Nullable IAndroidTarget target)450 public void initProject(@Nullable IProject project, @Nullable IAndroidTarget target) 451 throws IOException, StreamException { 452 if (project == null || target == null) { 453 return; 454 } 455 456 synchronized (LOCK) { 457 // check if there's already a state? 458 ProjectState state = getProjectState(project); 459 460 ProjectPropertiesWorkingCopy properties = null; 461 462 if (state != null) { 463 properties = state.getProperties().makeWorkingCopy(); 464 } 465 466 if (properties == null) { 467 IPath location = project.getLocation(); 468 if (location == null) { // can return null when the project is being deleted. 469 // do nothing and return null; 470 return; 471 } 472 473 properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT); 474 } 475 476 // save the target hash string in the project persistent property 477 properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); 478 properties.save(); 479 } 480 } 481 482 /** 483 * Returns the {@link ProjectState} object associated with a given project. 484 * <p/> 485 * This method is the only way to properly get the project's {@link ProjectState} 486 * If the project has not yet been loaded, then it is loaded. 487 * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk} 488 * objects, and therefore is static. 489 * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects 490 * are replaced. 491 * @param project the request project 492 * @return the ProjectState for the project. 493 */ 494 @Nullable 495 @SuppressWarnings("deprecation") getProjectState(IProject project)496 public static ProjectState getProjectState(IProject project) { 497 if (project == null) { 498 return null; 499 } 500 501 synchronized (LOCK) { 502 ProjectState state = sProjectStateMap.get(project); 503 if (state == null) { 504 // load the project.properties from the project folder. 505 IPath location = project.getLocation(); 506 if (location == null) { // can return null when the project is being deleted. 507 // do nothing and return null; 508 return null; 509 } 510 511 String projectLocation = location.toOSString(); 512 513 ProjectProperties properties = ProjectProperties.load(projectLocation, 514 PropertyType.PROJECT); 515 if (properties == null) { 516 // legacy support: look for default.properties and rename it if needed. 517 properties = ProjectProperties.load(projectLocation, 518 PropertyType.LEGACY_DEFAULT); 519 520 if (properties == null) { 521 AdtPlugin.log(IStatus.ERROR, 522 "Failed to load properties file for project '%s'", 523 project.getName()); 524 return null; 525 } else { 526 //legacy mode. 527 // get a working copy with the new type "project" 528 ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy( 529 PropertyType.PROJECT); 530 // and save it 531 try { 532 wc.save(); 533 534 // delete the old file. 535 ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT); 536 537 // make sure to use the new properties 538 properties = ProjectProperties.load(projectLocation, 539 PropertyType.PROJECT); 540 } catch (Exception e) { 541 AdtPlugin.log(IStatus.ERROR, 542 "Failed to rename properties file to %1$s for project '%s2$'", 543 PropertyType.PROJECT.getFilename(), project.getName()); 544 } 545 } 546 } 547 548 state = new ProjectState(project, properties); 549 sProjectStateMap.put(project, state); 550 551 // try to resolve the target 552 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) { 553 sCurrentSdk.loadTargetAndBuildTools(state); 554 } 555 } 556 557 return state; 558 } 559 } 560 561 /** 562 * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. 563 */ 564 @Nullable getTarget(IProject project)565 public IAndroidTarget getTarget(IProject project) { 566 if (project == null) { 567 return null; 568 } 569 570 ProjectState state = getProjectState(project); 571 if (state != null) { 572 return state.getTarget(); 573 } 574 575 return null; 576 } 577 578 /** 579 * Loads the {@link IAndroidTarget} and BuildTools for a given project. 580 * <p/>This method will get the target hash string from the project properties, and resolve 581 * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}. 582 * @param state the state representing the project to load. 583 * @return the target that was loaded. 584 */ 585 @Nullable loadTargetAndBuildTools(ProjectState state)586 public IAndroidTarget loadTargetAndBuildTools(ProjectState state) { 587 IAndroidTarget target = null; 588 if (state != null) { 589 String hash = state.getTargetHashString(); 590 if (hash != null) { 591 state.setTarget(target = getTargetFromHashString(hash)); 592 } 593 594 String markerMessage = null; 595 String buildToolInfoVersion = state.getBuildToolInfoVersion(); 596 if (buildToolInfoVersion != null) { 597 BuildToolInfo buildToolsInfo = getBuildToolInfo(buildToolInfoVersion); 598 599 if (buildToolsInfo != null) { 600 state.setBuildToolInfo(buildToolsInfo); 601 } else { 602 markerMessage = String.format("Unable to resolve %s property value '%s'", 603 ProjectProperties.PROPERTY_BUILD_TOOLS, 604 buildToolInfoVersion); 605 } 606 } else { 607 // this is ok, we'll use the latest one automatically. 608 state.setBuildToolInfo(null); 609 } 610 611 handleBuildToolsMarker(state.getProject(), markerMessage); 612 } 613 614 return target; 615 } 616 617 /** 618 * Adds or edit a build tools marker from the given project. This is done through a Job. 619 * @param project the project 620 * @param markerMessage the message. if null the marker is removed. 621 */ handleBuildToolsMarker(final IProject project, final String markerMessage)622 private void handleBuildToolsMarker(final IProject project, final String markerMessage) { 623 Job markerJob = new Job("Android SDK: Build Tools Marker") { 624 @Override 625 protected IStatus run(IProgressMonitor monitor) { 626 try { 627 if (project.isAccessible()) { 628 // always delete existing marker first 629 project.deleteMarkers(AdtConstants.MARKER_BUILD_TOOLS, true, 630 IResource.DEPTH_ZERO); 631 632 // add the new one if needed. 633 if (markerMessage != null) { 634 BaseProjectHelper.markProject(project, 635 AdtConstants.MARKER_BUILD_TOOLS, 636 markerMessage, IMarker.SEVERITY_ERROR, 637 IMarker.PRIORITY_HIGH); 638 } 639 } 640 } catch (CoreException e2) { 641 AdtPlugin.log(e2, null); 642 // Don't return e2.getStatus(); the job control will then produce 643 // a popup with this error, which isn't very interesting for the 644 // user. 645 } 646 647 return Status.OK_STATUS; 648 } 649 }; 650 651 // build jobs are run after other interactive jobs 652 markerJob.setPriority(Job.BUILD); 653 markerJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); 654 markerJob.schedule(); 655 } 656 657 /** 658 * Checks and loads (if needed) the data for a given target. 659 * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified 660 * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}. 661 * <p/>An optional project as second parameter can be given to be recompiled once the target 662 * data is finished loading. 663 * <p/>The return value is non-null only if the target data has already been loaded (and in this 664 * case is the status of the load operation) 665 * @param target the target to load. 666 * @param project an optional project to be recompiled when the target data is loaded. 667 * If the target is already loaded, nothing happens. 668 * @return The load status if the target data is already loaded. 669 */ 670 @NonNull checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project)671 public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) { 672 boolean loadData = false; 673 674 synchronized (LOCK) { 675 if (mDontLoadTargetData) { 676 return LoadStatus.FAILED; 677 } 678 679 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 680 if (bundle == null) { 681 bundle = new TargetLoadBundle(); 682 mTargetDataStatusMap.put(target,bundle); 683 684 // set status to loading 685 bundle.status = LoadStatus.LOADING; 686 687 // add project to bundle 688 if (project != null) { 689 bundle.projectsToReload.add(project); 690 } 691 692 // and set the flag to start the loading below 693 loadData = true; 694 } else if (bundle.status == LoadStatus.LOADING) { 695 // add project to bundle 696 if (project != null) { 697 bundle.projectsToReload.add(project); 698 } 699 700 return bundle.status; 701 } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) { 702 return bundle.status; 703 } 704 } 705 706 if (loadData) { 707 Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) { 708 @Override 709 protected IStatus run(IProgressMonitor monitor) { 710 AdtPlugin plugin = AdtPlugin.getDefault(); 711 try { 712 IStatus status = new AndroidTargetParser(target).run(monitor); 713 714 IJavaProject[] javaProjectArray = null; 715 716 synchronized (LOCK) { 717 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 718 719 if (status.getCode() != IStatus.OK) { 720 bundle.status = LoadStatus.FAILED; 721 bundle.projectsToReload.clear(); 722 } else { 723 bundle.status = LoadStatus.LOADED; 724 725 // Prepare the array of project to recompile. 726 // The call is done outside of the synchronized block. 727 javaProjectArray = bundle.projectsToReload.toArray( 728 new IJavaProject[bundle.projectsToReload.size()]); 729 730 // and update the UI of the editors that depend on the target data. 731 plugin.updateTargetListeners(target); 732 } 733 } 734 735 if (javaProjectArray != null) { 736 ProjectHelper.updateProjects(javaProjectArray); 737 } 738 739 return status; 740 } catch (Throwable t) { 741 synchronized (LOCK) { 742 TargetLoadBundle bundle = mTargetDataStatusMap.get(target); 743 bundle.status = LoadStatus.FAILED; 744 } 745 746 AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$ 747 String message = String.format("Parsing Data for %1$s failed", target.hashString()); 748 if (t instanceof UnsupportedClassVersionError) { 749 message = "To use this platform, run Eclipse with JDK 7 or later. (" + message + ")"; 750 } 751 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, t); 752 } 753 } 754 }; 755 job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs 756 job.setRule(ResourcesPlugin.getWorkspace().getRoot()); 757 job.schedule(); 758 } 759 760 // The only way to go through here is when the loading starts through the Job. 761 // Therefore the current status of the target is LOADING. 762 return LoadStatus.LOADING; 763 } 764 765 /** 766 * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. 767 */ 768 @Nullable getTargetData(IAndroidTarget target)769 public AndroidTargetData getTargetData(IAndroidTarget target) { 770 synchronized (LOCK) { 771 return mTargetDataMap.get(target); 772 } 773 } 774 775 /** 776 * Return the {@link AndroidTargetData} for a given {@link IProject}. 777 */ 778 @Nullable getTargetData(IProject project)779 public AndroidTargetData getTargetData(IProject project) { 780 synchronized (LOCK) { 781 IAndroidTarget target = getTarget(project); 782 if (target != null) { 783 return getTargetData(target); 784 } 785 } 786 787 return null; 788 } 789 790 /** 791 * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not 792 * loaded properly, then this will return <code>null</code>. 793 */ 794 @Nullable getDexWrapper(@ullable BuildToolInfo buildToolInfo)795 public DexWrapper getDexWrapper(@Nullable BuildToolInfo buildToolInfo) { 796 if (buildToolInfo == null) { 797 return null; 798 } 799 synchronized (LOCK) { 800 String dexLocation = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR); 801 DexWrapper dexWrapper = mDexWrappers.get(dexLocation); 802 803 if (dexWrapper == null) { 804 // load DX. 805 dexWrapper = new DexWrapper(); 806 IStatus res = dexWrapper.loadDex(dexLocation); 807 if (res != Status.OK_STATUS) { 808 AdtPlugin.log(null, res.getMessage()); 809 dexWrapper = null; 810 } else { 811 mDexWrappers.put(dexLocation, dexWrapper); 812 } 813 } 814 815 return dexWrapper; 816 } 817 } 818 unloadDexWrappers()819 public void unloadDexWrappers() { 820 synchronized (LOCK) { 821 for (DexWrapper wrapper : mDexWrappers.values()) { 822 wrapper.unload(); 823 } 824 mDexWrappers.clear(); 825 } 826 } 827 828 /** 829 * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could 830 * be <code>null</code>. 831 */ 832 @Nullable getAvdManager()833 public AvdManager getAvdManager() { 834 return mAvdManager; 835 } 836 837 @Nullable getDeviceVersion(@onNull IDevice device)838 public static AndroidVersion getDeviceVersion(@NonNull IDevice device) { 839 try { 840 Map<String, String> props = device.getProperties(); 841 String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL); 842 if (apiLevel == null) { 843 return null; 844 } 845 846 return new AndroidVersion(Integer.parseInt(apiLevel), 847 props.get((IDevice.PROP_BUILD_CODENAME))); 848 } catch (NumberFormatException e) { 849 return null; 850 } 851 } 852 853 @NonNull getDeviceManager()854 public DeviceManager getDeviceManager() { 855 return mDeviceManager; 856 } 857 858 /** 859 * Returns a list of {@link ProjectState} representing projects depending, directly or 860 * indirectly on a given library project. 861 * @param project the library project. 862 * @return a possibly empty list of ProjectState. 863 */ 864 @NonNull getMainProjectsFor(IProject project)865 public static Set<ProjectState> getMainProjectsFor(IProject project) { 866 synchronized (LOCK) { 867 // first get the project directly depending on this. 868 Set<ProjectState> list = new HashSet<ProjectState>(); 869 870 // loop on all project and see if ProjectState.getLibrary returns a non null 871 // project. 872 for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) { 873 if (project != entry.getKey()) { 874 LibraryState library = entry.getValue().getLibrary(project); 875 if (library != null) { 876 list.add(entry.getValue()); 877 } 878 } 879 } 880 881 // now look for projects depending on the projects directly depending on the library. 882 HashSet<ProjectState> result = new HashSet<ProjectState>(list); 883 for (ProjectState p : list) { 884 if (p.isLibrary()) { 885 Set<ProjectState> set = getMainProjectsFor(p.getProject()); 886 result.addAll(set); 887 } 888 } 889 890 return result; 891 } 892 } 893 894 /** 895 * Unload the SDK's target data. 896 * 897 * If <var>preventReload</var>, this effect is final until the SDK instance is changed 898 * through {@link #loadSdk(String)}. 899 * 900 * The goal is to unload the targets to be able to replace existing targets with new ones, 901 * before calling {@link #loadSdk(String)} to fully reload the SDK. 902 * 903 * @param preventReload prevent the data from being loaded again for the remaining live of 904 * this {@link Sdk} instance. 905 */ unloadTargetData(boolean preventReload)906 public void unloadTargetData(boolean preventReload) { 907 synchronized (LOCK) { 908 mDontLoadTargetData = preventReload; 909 910 // dispose of the target data. 911 for (AndroidTargetData data : mTargetDataMap.values()) { 912 data.dispose(); 913 } 914 915 mTargetDataMap.clear(); 916 } 917 } 918 Sdk(SdkManager manager, AvdManager avdManager)919 private Sdk(SdkManager manager, AvdManager avdManager) { 920 mManager = manager; 921 mAvdManager = avdManager; 922 923 // listen to projects closing 924 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); 925 // need to register the resource event listener first because the project listener 926 // is called back during registration with project opened in the workspace. 927 monitor.addResourceEventListener(mResourceEventListener); 928 monitor.addProjectListener(mProjectListener); 929 monitor.addFileListener(mFileListener, 930 IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED); 931 932 // pre-compute some paths 933 mDocBaseUrl = getDocumentationBaseUrl(manager.getLocation() + 934 SdkConstants.OS_SDK_DOCS_FOLDER); 935 936 mDeviceManager = DeviceManager.createInstance(manager.getLocalSdk().getLocation(), 937 AdtPlugin.getDefault()); 938 939 // update whatever ProjectState is already present with new IAndroidTarget objects. 940 synchronized (LOCK) { 941 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { 942 loadTargetAndBuildTools(entry.getValue()); 943 } 944 } 945 } 946 947 /** 948 * Cleans and unloads the SDK. 949 */ dispose()950 private void dispose() { 951 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); 952 monitor.removeProjectListener(mProjectListener); 953 monitor.removeFileListener(mFileListener); 954 monitor.removeResourceEventListener(mResourceEventListener); 955 956 // the IAndroidTarget objects are now obsolete so update the project states. 957 synchronized (LOCK) { 958 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { 959 entry.getValue().setTarget(null); 960 } 961 962 // dispose of the target data. 963 for (AndroidTargetData data : mTargetDataMap.values()) { 964 data.dispose(); 965 } 966 967 mTargetDataMap.clear(); 968 } 969 } 970 setTargetData(IAndroidTarget target, AndroidTargetData data)971 void setTargetData(IAndroidTarget target, AndroidTargetData data) { 972 synchronized (LOCK) { 973 mTargetDataMap.put(target, data); 974 } 975 } 976 977 /** 978 * Returns the URL to the local documentation. 979 * Can return null if no documentation is found in the current SDK. 980 * 981 * @param osDocsPath Path to the documentation folder in the current SDK. 982 * The folder may not actually exist. 983 * @return A file:// URL on the local documentation folder if it exists or null. 984 */ getDocumentationBaseUrl(String osDocsPath)985 private String getDocumentationBaseUrl(String osDocsPath) { 986 File f = new File(osDocsPath); 987 988 if (f.isDirectory()) { 989 try { 990 // Note: to create a file:// URL, one would typically use something like 991 // f.toURI().toURL().toString(). However this generates a broken path on 992 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of 993 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll 994 // do the correct thing manually. 995 996 String path = f.getAbsolutePath(); 997 if (File.separatorChar != '/') { 998 path = path.replace(File.separatorChar, '/'); 999 } 1000 1001 // For some reason the URL class doesn't add the mandatory "//" after 1002 // the "file:" protocol name, so it has to be hacked into the path. 1003 URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$ 1004 String result = url.toString(); 1005 return result; 1006 } catch (MalformedURLException e) { 1007 // ignore malformed URLs 1008 } 1009 } 1010 1011 return null; 1012 } 1013 1014 /** 1015 * Delegate listener for project changes. 1016 */ 1017 private IProjectListener mProjectListener = new IProjectListener() { 1018 @Override 1019 public void projectClosed(IProject project) { 1020 onProjectRemoved(project, false /*deleted*/); 1021 } 1022 1023 @Override 1024 public void projectDeleted(IProject project) { 1025 onProjectRemoved(project, true /*deleted*/); 1026 } 1027 1028 private void onProjectRemoved(IProject removedProject, boolean deleted) { 1029 if (DEBUG) { 1030 System.out.println(">>> CLOSED: " + removedProject.getName()); 1031 } 1032 1033 // get the target project 1034 synchronized (LOCK) { 1035 // Don't use getProject() as it could create the ProjectState if it's not 1036 // there yet and this is not what we want. We want the current object. 1037 // Therefore, direct access to the map. 1038 ProjectState removedState = sProjectStateMap.get(removedProject); 1039 if (removedState != null) { 1040 // 1. clear the layout lib cache associated with this project 1041 IAndroidTarget target = removedState.getTarget(); 1042 if (target != null) { 1043 // get the bridge for the target, and clear the cache for this project. 1044 AndroidTargetData data = mTargetDataMap.get(target); 1045 if (data != null) { 1046 LayoutLibrary layoutLib = data.getLayoutLibrary(); 1047 if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) { 1048 layoutLib.clearCaches(removedProject); 1049 } 1050 } 1051 } 1052 1053 // 2. if the project is a library, make sure to update the 1054 // LibraryState for any project referencing it. 1055 // Also, record the updated projects that are libraries, to update 1056 // projects that depend on them. 1057 for (ProjectState projectState : sProjectStateMap.values()) { 1058 LibraryState libState = projectState.getLibrary(removedProject); 1059 if (libState != null) { 1060 // Close the library right away. 1061 // This remove links between the LibraryState and the projectState. 1062 // This is because in case of a rename of a project, projectClosed and 1063 // projectOpened will be called before any other job is run, so we 1064 // need to make sure projectOpened is closed with the main project 1065 // state up to date. 1066 libState.close(); 1067 1068 // record that this project changed, and in case it's a library 1069 // that its parents need to be updated as well. 1070 markProject(projectState, projectState.isLibrary()); 1071 } 1072 } 1073 1074 // now remove the project for the project map. 1075 sProjectStateMap.remove(removedProject); 1076 } 1077 } 1078 1079 if (DEBUG) { 1080 System.out.println("<<<"); 1081 } 1082 } 1083 1084 @Override 1085 public void projectOpened(IProject project) { 1086 onProjectOpened(project); 1087 } 1088 1089 @Override 1090 public void projectOpenedWithWorkspace(IProject project) { 1091 // no need to force recompilation when projects are opened with the workspace. 1092 onProjectOpened(project); 1093 } 1094 1095 @Override 1096 public void allProjectsOpenedWithWorkspace() { 1097 // Correct currently open editors 1098 fixOpenLegacyEditors(); 1099 } 1100 1101 private void onProjectOpened(final IProject openedProject) { 1102 1103 ProjectState openedState = getProjectState(openedProject); 1104 if (openedState != null) { 1105 if (DEBUG) { 1106 System.out.println(">>> OPENED: " + openedProject.getName()); 1107 } 1108 1109 synchronized (LOCK) { 1110 final boolean isLibrary = openedState.isLibrary(); 1111 final boolean hasLibraries = openedState.hasLibraries(); 1112 1113 if (isLibrary || hasLibraries) { 1114 boolean foundLibraries = false; 1115 // loop on all the existing project and update them based on this new 1116 // project 1117 for (ProjectState projectState : sProjectStateMap.values()) { 1118 if (projectState != openedState) { 1119 // If the project has libraries, check if this project 1120 // is a reference. 1121 if (hasLibraries) { 1122 // ProjectState#needs() both checks if this is a missing library 1123 // and updates LibraryState to contains the new values. 1124 // This must always be called. 1125 LibraryState libState = openedState.needs(projectState); 1126 1127 if (libState != null) { 1128 // found a library! Add the main project to the list of 1129 // modified project 1130 foundLibraries = true; 1131 } 1132 } 1133 1134 // if the project is a library check if the other project depend 1135 // on it. 1136 if (isLibrary) { 1137 // ProjectState#needs() both checks if this is a missing library 1138 // and updates LibraryState to contains the new values. 1139 // This must always be called. 1140 LibraryState libState = projectState.needs(openedState); 1141 1142 if (libState != null) { 1143 // There's a dependency! Add the project to the list of 1144 // modified project, but also to a list of projects 1145 // that saw one of its dependencies resolved. 1146 markProject(projectState, projectState.isLibrary()); 1147 } 1148 } 1149 } 1150 } 1151 1152 // if the project has a libraries and we found at least one, we add 1153 // the project to the list of modified project. 1154 // Since we already went through the parent, no need to update them. 1155 if (foundLibraries) { 1156 markProject(openedState, false /*updateParents*/); 1157 } 1158 } 1159 } 1160 1161 // Correct file editor associations. 1162 fixEditorAssociations(openedProject); 1163 1164 // Fix classpath entries in a job since the workspace might be locked now. 1165 Job fixCpeJob = new Job("Adjusting Android Project Classpath") { 1166 @Override 1167 protected IStatus run(IProgressMonitor monitor) { 1168 try { 1169 ProjectHelper.fixProjectClasspathEntries( 1170 JavaCore.create(openedProject)); 1171 } catch (JavaModelException e) { 1172 AdtPlugin.log(e, "error fixing classpath entries"); 1173 // Don't return e2.getStatus(); the job control will then produce 1174 // a popup with this error, which isn't very interesting for the 1175 // user. 1176 } 1177 1178 return Status.OK_STATUS; 1179 } 1180 }; 1181 1182 // build jobs are run after other interactive jobs 1183 fixCpeJob.setPriority(Job.BUILD); 1184 fixCpeJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); 1185 fixCpeJob.schedule(); 1186 1187 1188 if (DEBUG) { 1189 System.out.println("<<<"); 1190 } 1191 } 1192 } 1193 1194 @Override 1195 public void projectRenamed(IProject project, IPath from) { 1196 // we don't actually care about this anymore. 1197 } 1198 }; 1199 1200 /** 1201 * Delegate listener for file changes. 1202 */ 1203 private IFileListener mFileListener = new IFileListener() { 1204 @Override 1205 public void fileChanged(final @NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, 1206 int kind, @Nullable String extension, int flags, boolean isAndroidPRoject) { 1207 if (!isAndroidPRoject) { 1208 return; 1209 } 1210 1211 if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) && 1212 file.getParent() == file.getProject()) { 1213 try { 1214 // reload the content of the project.properties file and update 1215 // the target. 1216 IProject iProject = file.getProject(); 1217 1218 ProjectState state = Sdk.getProjectState(iProject); 1219 1220 // get the current target and build tools 1221 IAndroidTarget oldTarget = state.getTarget(); 1222 boolean oldRsSupportMode = state.getRenderScriptSupportMode(); 1223 1224 // get the current library flag 1225 boolean wasLibrary = state.isLibrary(); 1226 1227 LibraryDifference diff = state.reloadProperties(); 1228 1229 // load the (possibly new) target. 1230 IAndroidTarget newTarget = loadTargetAndBuildTools(state); 1231 1232 // reload the libraries if needed 1233 if (diff.hasDiff()) { 1234 if (diff.added) { 1235 synchronized (LOCK) { 1236 for (ProjectState projectState : sProjectStateMap.values()) { 1237 if (projectState != state) { 1238 // need to call needs to do the libraryState link, 1239 // but no need to look at the result, as we'll compare 1240 // the result of getFullLibraryProjects() 1241 // this is easier to due to indirect dependencies. 1242 state.needs(projectState); 1243 } 1244 } 1245 } 1246 } 1247 1248 markProject(state, wasLibrary || state.isLibrary()); 1249 } 1250 1251 // apply the new target if needed. 1252 if (newTarget != oldTarget || 1253 oldRsSupportMode != state.getRenderScriptSupportMode()) { 1254 IJavaProject javaProject = BaseProjectHelper.getJavaProject( 1255 file.getProject()); 1256 if (javaProject != null) { 1257 ProjectHelper.updateProject(javaProject); 1258 } 1259 1260 // update the editors to reload with the new target 1261 AdtPlugin.getDefault().updateTargetListeners(iProject); 1262 } 1263 } catch (CoreException e) { 1264 // This can't happen as it's only for closed project (or non existing) 1265 // but in that case we can't get a fileChanged on this file. 1266 } 1267 } else if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) { 1268 // check if it's an add/remove on a jar files inside libs 1269 if (EXT_JAR.equals(extension) && 1270 file.getProjectRelativePath().segmentCount() == 2 && 1271 file.getParent().getName().equals(SdkConstants.FD_NATIVE_LIBS)) { 1272 // need to update the project and whatever depend on it. 1273 1274 processJarFileChange(file); 1275 } 1276 } 1277 } 1278 1279 private void processJarFileChange(final IFile file) { 1280 try { 1281 IProject iProject = file.getProject(); 1282 1283 if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 1284 return; 1285 } 1286 1287 List<IJavaProject> projectList = new ArrayList<IJavaProject>(); 1288 IJavaProject javaProject = BaseProjectHelper.getJavaProject(iProject); 1289 if (javaProject != null) { 1290 projectList.add(javaProject); 1291 } 1292 1293 ProjectState state = Sdk.getProjectState(iProject); 1294 1295 if (state != null) { 1296 Collection<ProjectState> parents = state.getFullParentProjects(); 1297 for (ProjectState s : parents) { 1298 javaProject = BaseProjectHelper.getJavaProject(s.getProject()); 1299 if (javaProject != null) { 1300 projectList.add(javaProject); 1301 } 1302 } 1303 1304 ProjectHelper.updateProjects( 1305 projectList.toArray(new IJavaProject[projectList.size()])); 1306 } 1307 } catch (CoreException e) { 1308 // This can't happen as it's only for closed project (or non existing) 1309 // but in that case we can't get a fileChanged on this file. 1310 } 1311 } 1312 }; 1313 1314 /** List of modified projects. This is filled in 1315 * {@link IProjectListener#projectOpened(IProject)}, 1316 * {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, 1317 * {@link IProjectListener#projectClosed(IProject)}, and 1318 * {@link IProjectListener#projectDeleted(IProject)} and processed in 1319 * {@link IResourceEventListener#resourceChangeEventEnd()}. 1320 */ 1321 private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>(); 1322 private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>(); 1323 markProject(ProjectState projectState, boolean updateParents)1324 private void markProject(ProjectState projectState, boolean updateParents) { 1325 if (mModifiedProjects.contains(projectState) == false) { 1326 if (DEBUG) { 1327 System.out.println("\tMARKED: " + projectState.getProject().getName()); 1328 } 1329 mModifiedProjects.add(projectState); 1330 } 1331 1332 // if the project is resolved also add it to this list. 1333 if (updateParents) { 1334 if (mModifiedChildProjects.contains(projectState) == false) { 1335 if (DEBUG) { 1336 System.out.println("\tMARKED(child): " + projectState.getProject().getName()); 1337 } 1338 mModifiedChildProjects.add(projectState); 1339 } 1340 } 1341 } 1342 1343 /** 1344 * Delegate listener for resource changes. This is called before and after any calls to the 1345 * project and file listeners (for a given resource change event). 1346 */ 1347 private IResourceEventListener mResourceEventListener = new IResourceEventListener() { 1348 @Override 1349 public void resourceChangeEventStart() { 1350 mModifiedProjects.clear(); 1351 mModifiedChildProjects.clear(); 1352 } 1353 1354 @Override 1355 public void resourceChangeEventEnd() { 1356 if (mModifiedProjects.size() == 0) { 1357 return; 1358 } 1359 1360 // first make sure all the parents are updated 1361 updateParentProjects(); 1362 1363 // for all modified projects, update their library list 1364 // and gather their IProject 1365 final List<IJavaProject> projectList = new ArrayList<IJavaProject>(); 1366 for (ProjectState state : mModifiedProjects) { 1367 state.updateFullLibraryList(); 1368 projectList.add(JavaCore.create(state.getProject())); 1369 } 1370 1371 Job job = new Job("Android Library Update") { //$NON-NLS-1$ 1372 @Override 1373 protected IStatus run(IProgressMonitor monitor) { 1374 LibraryClasspathContainerInitializer.updateProjects( 1375 projectList.toArray(new IJavaProject[projectList.size()])); 1376 1377 for (IJavaProject javaProject : projectList) { 1378 try { 1379 javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, 1380 monitor); 1381 } catch (CoreException e) { 1382 // pass 1383 } 1384 } 1385 return Status.OK_STATUS; 1386 } 1387 }; 1388 job.setPriority(Job.BUILD); 1389 job.setRule(ResourcesPlugin.getWorkspace().getRoot()); 1390 job.schedule(); 1391 } 1392 }; 1393 1394 /** 1395 * Updates all existing projects with a given list of new/updated libraries. 1396 * This loops through all opened projects and check if they depend on any of the given 1397 * library project, and if they do, they are linked together. 1398 */ updateParentProjects()1399 private void updateParentProjects() { 1400 if (mModifiedChildProjects.size() == 0) { 1401 return; 1402 } 1403 1404 ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects); 1405 mModifiedChildProjects.clear(); 1406 synchronized (LOCK) { 1407 // for each project for which we must update its parent, we loop on the parent 1408 // projects and adds them to the list of modified projects. If they are themselves 1409 // libraries, we add them too. 1410 for (ProjectState state : childProjects) { 1411 if (DEBUG) { 1412 System.out.println(">>> Updating parents of " + state.getProject().getName()); 1413 } 1414 List<ProjectState> parents = state.getParentProjects(); 1415 for (ProjectState parent : parents) { 1416 markProject(parent, parent.isLibrary()); 1417 } 1418 if (DEBUG) { 1419 System.out.println("<<<"); 1420 } 1421 } 1422 } 1423 1424 // done, but there may be parents that are also libraries. Need to update their parents. 1425 updateParentProjects(); 1426 } 1427 1428 /** 1429 * Fix editor associations for the given project, if not already done. 1430 * <p/> 1431 * Eclipse has a per-file setting for which editor should be used for each file 1432 * (see {@link IDE#setDefaultEditor(IFile, String)}). 1433 * We're using this flag to pick between the various XML editors (layout, drawable, etc) 1434 * since they all have the same file name extension. 1435 * <p/> 1436 * Unfortunately, the file setting can be "wrong" for two reasons: 1437 * <ol> 1438 * <li> The editor type was added <b>after</b> a file had been seen by the IDE. 1439 * For example, we added new editors for animations and for drawables around 1440 * ADT 12, but any file seen by ADT in earlier versions will continue to use 1441 * the vanilla Eclipse XML editor instead. 1442 * <li> A bug in ADT 14 and ADT 15 (see issue 21124) meant that files created in new 1443 * folders would end up with wrong editor associations. Even though that bug 1444 * is fixed in ADT 16, the fix only affects new files, it cannot retroactively 1445 * fix editor associations that were set incorrectly by ADT 14 or 15. 1446 * </ol> 1447 * <p/> 1448 * This method attempts to fix the editor bindings retroactively by scanning all the 1449 * resource XML files and resetting the editor associations. 1450 * Since this is a potentially slow operation, this is only done "once"; we use a 1451 * persistent project property to avoid looking repeatedly. In the future if we add 1452 * additional editors, we can rev the scanned version value. 1453 */ fixEditorAssociations(final IProject project)1454 private void fixEditorAssociations(final IProject project) { 1455 QualifiedName KEY = new QualifiedName(AdtPlugin.PLUGIN_ID, "editorbinding"); //$NON-NLS-1$ 1456 1457 try { 1458 String value = project.getPersistentProperty(KEY); 1459 int currentVersion = 0; 1460 if (value != null) { 1461 try { 1462 currentVersion = Integer.parseInt(value); 1463 } catch (Exception ingore) { 1464 } 1465 } 1466 1467 // The target version we're comparing to. This must be incremented each time 1468 // we change the processing here so that a new version of the plugin would 1469 // try to fix existing user projects. 1470 final int targetVersion = 2; 1471 1472 if (currentVersion >= targetVersion) { 1473 return; 1474 } 1475 1476 // Set to specific version such that we can rev the version in the future 1477 // to trigger further scanning 1478 project.setPersistentProperty(KEY, Integer.toString(targetVersion)); 1479 1480 // Now update the actual editor associations. 1481 Job job = new Job("Update Android editor bindings") { //$NON-NLS-1$ 1482 @Override 1483 protected IStatus run(IProgressMonitor monitor) { 1484 try { 1485 for (IResource folderResource : project.getFolder(FD_RES).members()) { 1486 if (folderResource instanceof IFolder) { 1487 IFolder folder = (IFolder) folderResource; 1488 1489 for (IResource resource : folder.members()) { 1490 if (resource instanceof IFile && 1491 resource.getName().endsWith(DOT_XML)) { 1492 fixXmlFile((IFile) resource); 1493 } 1494 } 1495 } 1496 } 1497 1498 // TODO change AndroidManifest.xml ID too 1499 1500 } catch (CoreException e) { 1501 AdtPlugin.log(e, null); 1502 } 1503 1504 return Status.OK_STATUS; 1505 } 1506 1507 /** 1508 * Attempt to fix the editor ID for the given /res XML file. 1509 */ 1510 private void fixXmlFile(final IFile file) { 1511 // Fix the default editor ID for this resource. 1512 // This has no effect on currently open editors. 1513 IEditorDescriptor desc = IDE.getDefaultEditor(file); 1514 1515 if (desc == null || !CommonXmlEditor.ID.equals(desc.getId())) { 1516 IDE.setDefaultEditor(file, CommonXmlEditor.ID); 1517 } 1518 } 1519 }; 1520 job.setPriority(Job.BUILD); 1521 job.schedule(); 1522 } catch (CoreException e) { 1523 AdtPlugin.log(e, null); 1524 } 1525 } 1526 1527 /** 1528 * Tries to fix all currently open Android legacy editors. 1529 * <p/> 1530 * If an editor is found to match one of the legacy ids, we'll try to close it. 1531 * If that succeeds, we try to reopen it using the new common editor ID. 1532 * <p/> 1533 * This method must be run from the UI thread. 1534 */ fixOpenLegacyEditors()1535 private void fixOpenLegacyEditors() { 1536 1537 AdtPlugin adt = AdtPlugin.getDefault(); 1538 if (adt == null) { 1539 return; 1540 } 1541 1542 final IPreferenceStore store = adt.getPreferenceStore(); 1543 int currentValue = store.getInt(AdtPrefs.PREFS_FIX_LEGACY_EDITORS); 1544 // The target version we're comparing to. This must be incremented each time 1545 // we change the processing here so that a new version of the plugin would 1546 // try to fix existing editors. 1547 final int targetValue = 1; 1548 1549 if (currentValue >= targetValue) { 1550 return; 1551 } 1552 1553 // To be able to close and open editors we need to make sure this is done 1554 // in the UI thread, which this isn't invoked from. 1555 PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { 1556 @Override 1557 public void run() { 1558 HashSet<String> legacyIds = 1559 new HashSet<String>(Arrays.asList(CommonXmlEditor.LEGACY_EDITOR_IDS)); 1560 1561 for (IWorkbenchWindow win : PlatformUI.getWorkbench().getWorkbenchWindows()) { 1562 for (IWorkbenchPage page : win.getPages()) { 1563 for (IEditorReference ref : page.getEditorReferences()) { 1564 try { 1565 IEditorInput input = ref.getEditorInput(); 1566 if (input instanceof IFileEditorInput) { 1567 IFile file = ((IFileEditorInput)input).getFile(); 1568 IEditorPart part = ref.getEditor(true /*restore*/); 1569 if (part != null) { 1570 IWorkbenchPartSite site = part.getSite(); 1571 if (site != null) { 1572 String id = site.getId(); 1573 if (legacyIds.contains(id)) { 1574 // This editor matches one of legacy editor IDs. 1575 fixEditor(page, part, input, file, id); 1576 } 1577 } 1578 } 1579 } 1580 } catch (Exception e) { 1581 // ignore 1582 } 1583 } 1584 } 1585 } 1586 1587 // Remember that we managed to do fix all editors 1588 store.setValue(AdtPrefs.PREFS_FIX_LEGACY_EDITORS, targetValue); 1589 } 1590 1591 private void fixEditor( 1592 IWorkbenchPage page, 1593 IEditorPart part, 1594 IEditorInput input, 1595 IFile file, 1596 String id) { 1597 IDE.setDefaultEditor(file, CommonXmlEditor.ID); 1598 1599 boolean ok = page.closeEditor(part, true /*save*/); 1600 1601 AdtPlugin.log(IStatus.INFO, 1602 "Closed legacy editor ID %s for %s: %s", //$NON-NLS-1$ 1603 id, 1604 file.getFullPath(), 1605 ok ? "Success" : "Failed");//$NON-NLS-1$ //$NON-NLS-2$ 1606 1607 if (ok) { 1608 // Try to reopen it with the new ID 1609 try { 1610 page.openEditor(input, CommonXmlEditor.ID); 1611 } catch (PartInitException e) { 1612 AdtPlugin.log(e, 1613 "Failed to reopen %s", //$NON-NLS-1$ 1614 file.getFullPath()); 1615 } 1616 } 1617 } 1618 }); 1619 } 1620 } 1621