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