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