1 /* 2 * Copyright (C) 2007 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.project; 18 19 import com.android.SdkConstants; 20 import com.android.ide.common.sdk.LoadStatus; 21 import com.android.ide.eclipse.adt.AdtConstants; 22 import com.android.ide.eclipse.adt.AdtPlugin; 23 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 24 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 25 import com.android.sdklib.AndroidVersion; 26 import com.android.sdklib.IAndroidTarget; 27 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 28 import com.google.common.collect.Maps; 29 import com.google.common.io.Closeables; 30 31 import org.eclipse.core.resources.IProject; 32 import org.eclipse.core.resources.IWorkspaceRoot; 33 import org.eclipse.core.resources.ResourcesPlugin; 34 import org.eclipse.core.runtime.CoreException; 35 import org.eclipse.core.runtime.FileLocator; 36 import org.eclipse.core.runtime.IPath; 37 import org.eclipse.core.runtime.NullProgressMonitor; 38 import org.eclipse.core.runtime.Path; 39 import org.eclipse.core.runtime.Platform; 40 import org.eclipse.jdt.core.IAccessRule; 41 import org.eclipse.jdt.core.IClasspathAttribute; 42 import org.eclipse.jdt.core.IClasspathContainer; 43 import org.eclipse.jdt.core.IClasspathEntry; 44 import org.eclipse.jdt.core.IJavaModel; 45 import org.eclipse.jdt.core.IJavaProject; 46 import org.eclipse.jdt.core.JavaCore; 47 import org.eclipse.jdt.core.JavaModelException; 48 import org.osgi.framework.Bundle; 49 50 import java.io.File; 51 import java.io.IOException; 52 import java.io.InputStream; 53 import java.net.URI; 54 import java.net.URISyntaxException; 55 import java.net.URL; 56 import java.net.URLConnection; 57 import java.util.ArrayList; 58 import java.util.HashSet; 59 import java.util.Map; 60 import java.util.regex.Pattern; 61 62 /** 63 * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to 64 * {@link IProject}s. This removes the hard-coded path to the android.jar. 65 */ 66 public class AndroidClasspathContainerInitializer extends BaseClasspathContainerInitializer { 67 68 public static final String NULL_API_URL = "<null>"; //$NON-NLS-1$ 69 70 public static final String SOURCES_ZIP = "/sources.zip"; //$NON-NLS-1$ 71 72 public static final String COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE = 73 "com.android.ide.eclipse.source"; //$NON-NLS-1$ 74 75 private static final String ANDROID_API_REFERENCE = 76 "http://developer.android.com/reference/"; //$NON-NLS-1$ 77 78 private final static String PROPERTY_ANDROID_API = "androidApi"; //$NON-NLS-1$ 79 80 private final static String PROPERTY_ANDROID_SOURCE = "androidSource"; //$NON-NLS-1$ 81 82 /** path separator to store multiple paths in a single property. This is guaranteed to not 83 * be in a path. 84 */ 85 private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ 86 87 private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ 88 private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ 89 private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ 90 private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; 91 92 private final static int CACHE_INDEX_JAR = 0; 93 private final static int CACHE_INDEX_SRC = 1; 94 private final static int CACHE_INDEX_DOCS_URI = 2; 95 private final static int CACHE_INDEX_OPT_DOCS_URI = 3; 96 private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; 97 98 public AndroidClasspathContainerInitializer() { 99 // pass 100 } 101 102 /** 103 * Binds a classpath container to a {@link IClasspathContainer} for a given project, 104 * or silently fails if unable to do so. 105 * @param containerPath the container path that is the container id. 106 * @param project the project to bind 107 */ 108 @Override 109 public void initialize(IPath containerPath, IJavaProject project) throws CoreException { 110 if (AdtConstants.CONTAINER_FRAMEWORK.equals(containerPath.toString())) { 111 IClasspathContainer container = allocateAndroidContainer(project); 112 if (container != null) { 113 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_FRAMEWORK), 114 new IJavaProject[] { project }, 115 new IClasspathContainer[] { container }, 116 new NullProgressMonitor()); 117 } 118 } 119 } 120 121 /** 122 * Updates the {@link IJavaProject} objects with new android framework container. This forces 123 * JDT to recompile them. 124 * @param androidProjects the projects to update. 125 * @return <code>true</code> if success, <code>false</code> otherwise. 126 */ 127 static boolean updateProjects(IJavaProject[] androidProjects) { 128 try { 129 // Allocate a new AndroidClasspathContainer, and associate it to the android framework 130 // container id for each projects. 131 // By providing a new association between a container id and a IClasspathContainer, 132 // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with 133 // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of 134 // the projects. 135 int projectCount = androidProjects.length; 136 137 IClasspathContainer[] containers = new IClasspathContainer[projectCount]; 138 for (int i = 0 ; i < projectCount; i++) { 139 containers[i] = allocateAndroidContainer(androidProjects[i]); 140 } 141 142 // give each project their new container in one call. 143 JavaCore.setClasspathContainer( 144 new Path(AdtConstants.CONTAINER_FRAMEWORK), 145 androidProjects, containers, new NullProgressMonitor()); 146 147 return true; 148 } catch (JavaModelException e) { 149 return false; 150 } 151 } 152 153 /** 154 * Allocates and returns an {@link AndroidClasspathContainer} object with the proper 155 * path to the framework jar file. 156 * @param javaProject The java project that will receive the container. 157 */ 158 private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { 159 final IProject iProject = javaProject.getProject(); 160 161 String markerMessage = null; 162 boolean outputToConsole = true; 163 IAndroidTarget target = null; 164 165 try { 166 AdtPlugin plugin = AdtPlugin.getDefault(); 167 if (plugin == null) { // This is totally weird, but I've seen it happen! 168 return null; 169 } 170 171 synchronized (Sdk.getLock()) { 172 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 173 174 // check if the project has a valid target. 175 ProjectState state = Sdk.getProjectState(iProject); 176 if (state == null) { 177 // looks like the project state (project.properties) couldn't be read! 178 markerMessage = String.format( 179 "Project has no %1$s file! Edit the project properties to set one.", 180 SdkConstants.FN_PROJECT_PROPERTIES); 181 } else { 182 // this might be null if the sdk is not yet loaded. 183 target = state.getTarget(); 184 185 // if we are loaded and the target is non null, we create a valid 186 // ClassPathContainer 187 if (sdkIsLoaded && target != null) { 188 // first make sure the target has loaded its data 189 Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/); 190 191 String targetName = target.getClasspathName(); 192 193 return new AndroidClasspathContainer( 194 createClasspathEntries(iProject, target, targetName), 195 new Path(AdtConstants.CONTAINER_FRAMEWORK), 196 targetName, 197 IClasspathContainer.K_DEFAULT_SYSTEM); 198 } 199 200 // In case of error, we'll try different thing to provide the best error message 201 // possible. 202 // Get the project's target's hash string (if it exists) 203 String hashString = state.getTargetHashString(); 204 205 if (hashString == null || hashString.length() == 0) { 206 // if there is no hash string we only show this if the SDK is loaded. 207 // For a project opened at start-up with no target, this would be displayed 208 // twice, once when the project is opened, and once after the SDK has 209 // finished loading. 210 // By testing the sdk is loaded, we only show this once in the console. 211 if (sdkIsLoaded) { 212 markerMessage = String.format( 213 "Project has no target set. Edit the project properties to set one."); 214 } 215 } else if (sdkIsLoaded) { 216 markerMessage = String.format( 217 "Unable to resolve target '%s'", hashString); 218 } else { 219 // this is the case where there is a hashString but the SDK is not yet 220 // loaded and therefore we can't get the target yet. 221 // We check if there is a cache of the needed information. 222 AndroidClasspathContainer container = getContainerFromCache(iProject, 223 target); 224 225 if (container == null) { 226 // either the cache was wrong (ie folder does not exists anymore), or 227 // there was no cache. In this case we need to make sure the project 228 // is resolved again after the SDK is loaded. 229 plugin.setProjectToResolve(javaProject); 230 231 markerMessage = String.format( 232 "Unable to resolve target '%s' until the SDK is loaded.", 233 hashString); 234 235 // let's not log this one to the console as it will happen at 236 // every boot, and it's expected. (we do keep the error marker though). 237 outputToConsole = false; 238 239 } else { 240 // we created a container from the cache, so we register the project 241 // to be checked for cache validity once the SDK is loaded 242 plugin.setProjectToCheck(javaProject); 243 244 // and return the container 245 return container; 246 } 247 } 248 } 249 250 // return a dummy container to replace the one we may have had before. 251 // It'll be replaced by the real when if/when the target is resolved if/when the 252 // SDK finishes loading. 253 return new IClasspathContainer() { 254 @Override 255 public IClasspathEntry[] getClasspathEntries() { 256 return new IClasspathEntry[0]; 257 } 258 259 @Override 260 public String getDescription() { 261 return "Unable to get system library for the project"; 262 } 263 264 @Override 265 public int getKind() { 266 return IClasspathContainer.K_DEFAULT_SYSTEM; 267 } 268 269 @Override 270 public IPath getPath() { 271 return null; 272 } 273 }; 274 } 275 } finally { 276 processError(iProject, markerMessage, AdtConstants.MARKER_TARGET, outputToConsole); 277 } 278 } 279 280 /** 281 * Creates and returns an array of {@link IClasspathEntry} objects for the android 282 * framework and optional libraries. 283 * <p/>This references the OS path to the android.jar and the 284 * java doc directory. This is dynamically created when a project is opened, 285 * and never saved in the project itself, so there's no risk of storing an 286 * obsolete path. 287 * The method also stores the paths used to create the entries in the project persistent 288 * properties. A new {@link AndroidClasspathContainer} can be created from the stored path 289 * using the {@link #getContainerFromCache(IProject)} method. 290 * @param project 291 * @param target The target that contains the libraries. 292 * @param targetName 293 */ 294 private static IClasspathEntry[] createClasspathEntries(IProject project, 295 IAndroidTarget target, String targetName) { 296 297 // get the path from the target 298 String[] paths = getTargetPaths(target); 299 300 // create the classpath entry from the paths 301 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target); 302 303 // paths now contains all the path required to recreate the IClasspathEntry with no 304 // target info. We encode them in a single string, with each path separated by 305 // OS path separator. 306 StringBuilder sb = new StringBuilder(CACHE_VERSION); 307 for (String p : paths) { 308 sb.append(PATH_SEPARATOR); 309 sb.append(p); 310 } 311 312 // store this in a project persistent property 313 ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); 314 ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); 315 316 return entries; 317 } 318 319 /** 320 * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. 321 */ 322 private static AndroidClasspathContainer getContainerFromCache(IProject project, 323 IAndroidTarget target) { 324 // get the cached info from the project persistent properties. 325 String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); 326 String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); 327 if (cache == null || targetNameCache == null) { 328 return null; 329 } 330 331 // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. 332 if (cache.startsWith(CACHE_VERSION_SEP) == false) { 333 return null; 334 } 335 336 cache = cache.substring(CACHE_VERSION_SEP.length()); 337 338 // the cache contains multiple paths, separated by a character guaranteed to not be in 339 // the path (\u001C). 340 // The first 3 are for android.jar (jar, source, doc), the rest are for the optional 341 // libraries and should contain at least one doc and a jar (if there are any libraries). 342 // Therefore, the path count should be 3 or 5+ 343 String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); 344 if (paths.length < 3 || paths.length == 4) { 345 return null; 346 } 347 348 // now we check the paths actually exist. 349 // There's an exception: If the source folder for android.jar does not exist, this is 350 // not a problem, so we skip it. 351 // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a 352 // bit differently. 353 try { 354 if (new File(paths[CACHE_INDEX_JAR]).exists() == false || 355 new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { 356 return null; 357 } 358 359 // check the path for the add-ons, if they exist. 360 if (paths.length > CACHE_INDEX_ADD_ON_START) { 361 362 // check the docs path separately from the rest of the paths as it's a URI. 363 if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { 364 return null; 365 } 366 367 // now just check the remaining paths. 368 for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { 369 String path = paths[i]; 370 if (path.length() > 0) { 371 File f = new File(path); 372 if (f.exists() == false) { 373 return null; 374 } 375 } 376 } 377 } 378 } catch (URISyntaxException e) { 379 return null; 380 } 381 382 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target); 383 384 return new AndroidClasspathContainer(entries, 385 new Path(AdtConstants.CONTAINER_FRAMEWORK), 386 targetNameCache, IClasspathContainer.K_DEFAULT_SYSTEM); 387 } 388 389 /** 390 * Generates an array of {@link IClasspathEntry} from a set of paths. 391 * @see #getTargetPaths(IAndroidTarget) 392 */ 393 private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths, 394 IAndroidTarget target) { 395 ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>(); 396 397 // First, we create the IClasspathEntry for the framework. 398 // now add the android framework to the class path. 399 // create the path object. 400 IPath androidLib = new Path(paths[CACHE_INDEX_JAR]); 401 402 IPath androidSrc = null; 403 String androidSrcOsPath = null; 404 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); 405 if (target != null) { 406 androidSrcOsPath = 407 ProjectHelper.loadStringProperty(root, getAndroidSourceProperty(target)); 408 } 409 if (androidSrcOsPath != null && androidSrcOsPath.trim().length() > 0) { 410 androidSrc = new Path(androidSrcOsPath); 411 } 412 if (androidSrc == null) { 413 androidSrc = new Path(paths[CACHE_INDEX_SRC]); 414 File androidSrcFile = new File(paths[CACHE_INDEX_SRC]); 415 if (!androidSrcFile.isDirectory()) { 416 androidSrc = null; 417 } 418 } 419 420 if (androidSrc == null && target != null) { 421 Bundle bundle = getSourceBundle(); 422 423 if (bundle != null) { 424 AndroidVersion version = target.getVersion(); 425 String apiString = version.getApiString(); 426 String sourcePath = apiString + SOURCES_ZIP; 427 URL sourceURL = bundle.getEntry(sourcePath); 428 if (sourceURL != null) { 429 URL url = null; 430 try { 431 url = FileLocator.resolve(sourceURL); 432 } catch (IOException ignore) { 433 } 434 if (url != null) { 435 androidSrcOsPath = url.getFile(); 436 if (new File(androidSrcOsPath).isFile()) { 437 androidSrc = new Path(androidSrcOsPath); 438 } 439 } 440 } 441 } 442 } 443 444 // create the java doc link. 445 String androidApiURL = ProjectHelper.loadStringProperty(root, PROPERTY_ANDROID_API); 446 String apiURL = null; 447 if (androidApiURL != null && testURL(androidApiURL)) { 448 apiURL = androidApiURL; 449 } else { 450 if (testURL(paths[CACHE_INDEX_DOCS_URI])) { 451 apiURL = paths[CACHE_INDEX_DOCS_URI]; 452 } else if (testURL(ANDROID_API_REFERENCE)) { 453 apiURL = ANDROID_API_REFERENCE; 454 } 455 } 456 457 IClasspathAttribute[] attributes = null; 458 if (apiURL != null && !NULL_API_URL.equals(apiURL)) { 459 IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( 460 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, apiURL); 461 attributes = new IClasspathAttribute[] { 462 cpAttribute 463 }; 464 } 465 // create the access rule to restrict access to classes in 466 // com.android.internal 467 IAccessRule accessRule = JavaCore.newAccessRule(new Path("com/android/internal/**"), //$NON-NLS-1$ 468 IAccessRule.K_NON_ACCESSIBLE); 469 470 IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(androidLib, 471 androidSrc, // source attachment path 472 null, // default source attachment root path. 473 new IAccessRule[] { accessRule }, 474 attributes, 475 false // not exported. 476 ); 477 478 list.add(frameworkClasspathEntry); 479 480 // now deal with optional libraries 481 if (paths.length >= 5) { 482 String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; 483 int i = 4; 484 while (i < paths.length) { 485 Path jarPath = new Path(paths[i++]); 486 487 attributes = null; 488 if (docPath.length() > 0) { 489 attributes = new IClasspathAttribute[] { 490 JavaCore.newClasspathAttribute( 491 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, docPath) 492 }; 493 } 494 495 IClasspathEntry entry = JavaCore.newLibraryEntry( 496 jarPath, 497 null, // source attachment path 498 null, // default source attachment root path. 499 null, 500 attributes, 501 false // not exported. 502 ); 503 list.add(entry); 504 } 505 } 506 507 if (apiURL != null) { 508 ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, apiURL); 509 } 510 if (androidSrc != null && target != null) { 511 ProjectHelper.saveStringProperty(root, getAndroidSourceProperty(target), 512 androidSrc.toOSString()); 513 } 514 return list.toArray(new IClasspathEntry[list.size()]); 515 } 516 517 private static Bundle getSourceBundle() { 518 String bundleId = System.getProperty(COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE, 519 COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE); 520 Bundle bundle = Platform.getBundle(bundleId); 521 return bundle; 522 } 523 524 private static String getAndroidSourceProperty(IAndroidTarget target) { 525 if (target == null) { 526 return null; 527 } 528 String androidSourceProperty = PROPERTY_ANDROID_SOURCE + "_" 529 + target.getVersion().getApiString(); 530 return androidSourceProperty; 531 } 532 533 /** 534 * Cache results for testURL: Some are expensive to compute, and this is 535 * called repeatedly (perhaps for each open project) 536 */ 537 private static final Map<String, Boolean> sRecentUrlValidCache = 538 Maps.newHashMapWithExpectedSize(4); 539 540 @SuppressWarnings("resource") // Eclipse does not handle Closeables#closeQuietly 541 private static boolean testURL(String androidApiURL) { 542 Boolean cached = sRecentUrlValidCache.get(androidApiURL); 543 if (cached != null) { 544 return cached.booleanValue(); 545 } 546 boolean valid = false; 547 InputStream is = null; 548 try { 549 URL testURL = new URL(androidApiURL); 550 URLConnection connection = testURL.openConnection(); 551 // Only try for 5 seconds (though some implementations ignore this flag) 552 connection.setConnectTimeout(5000); 553 connection.setReadTimeout(5000); 554 is = connection.getInputStream(); 555 valid = true; 556 } catch (Exception ignore) { 557 } finally { 558 Closeables.closeQuietly(is); 559 } 560 561 sRecentUrlValidCache.put(androidApiURL, valid); 562 563 return valid; 564 } 565 566 /** 567 * Checks the projects' caches. If the cache was valid, the project is removed from the list. 568 * @param projects the list of projects to check. 569 */ 570 public static void checkProjectsCache(ArrayList<IJavaProject> projects) { 571 Sdk currentSdk = Sdk.getCurrent(); 572 int i = 0; 573 projectLoop: while (i < projects.size()) { 574 IJavaProject javaProject = projects.get(i); 575 IProject iProject = javaProject.getProject(); 576 577 // check if the project is opened 578 if (iProject.isOpen() == false) { 579 // remove from the list 580 // we do not increment i in this case. 581 projects.remove(i); 582 583 continue; 584 } 585 586 // project that have been resolved before the sdk was loaded 587 // will have a ProjectState where the IAndroidTarget is null 588 // so we load the target now that the SDK is loaded. 589 IAndroidTarget target = currentSdk.loadTargetAndBuildTools( 590 Sdk.getProjectState(iProject)); 591 if (target == null) { 592 // this is really not supposed to happen. This would mean there are cached paths, 593 // but project.properties was deleted. Keep the project in the list to force 594 // a resolve which will display the error. 595 i++; 596 continue; 597 } 598 599 String[] targetPaths = getTargetPaths(target); 600 601 // now get the cached paths 602 String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); 603 if (cache == null) { 604 // this should not happen. We'll force resolve again anyway. 605 i++; 606 continue; 607 } 608 609 String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); 610 if (cachedPaths.length < 3 || cachedPaths.length == 4) { 611 // paths length is wrong. simply resolve the project again 612 i++; 613 continue; 614 } 615 616 // Now we compare the paths. The first 4 can be compared directly. 617 // because of case sensitiveness we need to use File objects 618 619 if (targetPaths.length != cachedPaths.length) { 620 // different paths, force resolve again. 621 i++; 622 continue; 623 } 624 625 // compare the main paths (android.jar, main sources, main javadoc) 626 if (new File(targetPaths[CACHE_INDEX_JAR]).equals( 627 new File(cachedPaths[CACHE_INDEX_JAR])) == false || 628 new File(targetPaths[CACHE_INDEX_SRC]).equals( 629 new File(cachedPaths[CACHE_INDEX_SRC])) == false || 630 new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( 631 new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { 632 // different paths, force resolve again. 633 i++; 634 continue; 635 } 636 637 if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { 638 // compare optional libraries javadoc 639 if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( 640 new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { 641 // different paths, force resolve again. 642 i++; 643 continue; 644 } 645 646 // testing the optional jar files is a little bit trickier. 647 // The order is not guaranteed to be identical. 648 // From a previous test, we do know however that there is the same number. 649 // The number of libraries should be low enough that we can simply go through the 650 // lists manually. 651 targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { 652 String targetPath = targetPaths[tpi]; 653 654 // look for a match in the other array 655 for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { 656 if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { 657 // found a match. Try the next targetPath 658 continue targetLoop; 659 } 660 } 661 662 // if we stop here, we haven't found a match, which means there's a 663 // discrepancy in the libraries. We force a resolve. 664 i++; 665 continue projectLoop; 666 } 667 } 668 669 // at the point the check passes, and we can remove the project from the list. 670 // we do not increment i in this case. 671 projects.remove(i); 672 } 673 } 674 675 /** 676 * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. 677 * <p/>The paths are always in the same order. 678 * <ul> 679 * <li>Path to android.jar</li> 680 * <li>Path to the source code for android.jar</li> 681 * <li>Path to the javadoc for the android platform</li> 682 * </ul> 683 * Additionally, if there are optional libraries, the array will contain: 684 * <ul> 685 * <li>Path to the libraries javadoc</li> 686 * <li>Path to the first .jar file</li> 687 * <li>(more .jar as needed)</li> 688 * </ul> 689 */ 690 private static String[] getTargetPaths(IAndroidTarget target) { 691 ArrayList<String> paths = new ArrayList<String>(); 692 693 // first, we get the path for android.jar 694 // The order is: android.jar, source folder, docs folder 695 paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); 696 paths.add(target.getPath(IAndroidTarget.SOURCES)); 697 paths.add(AdtPlugin.getUrlDoc()); 698 699 // now deal with optional libraries. 700 IOptionalLibrary[] libraries = target.getOptionalLibraries(); 701 if (libraries != null) { 702 // all the optional libraries use the same javadoc, so we start with this 703 String targetDocPath = target.getPath(IAndroidTarget.DOCS); 704 if (targetDocPath != null) { 705 paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); 706 } else { 707 // we add an empty string, to always have the same count. 708 paths.add(""); 709 } 710 711 // because different libraries could use the same jar file, we make sure we add 712 // each jar file only once. 713 HashSet<String> visitedJars = new HashSet<String>(); 714 for (IOptionalLibrary library : libraries) { 715 String jarPath = library.getJarPath(); 716 if (visitedJars.contains(jarPath) == false) { 717 visitedJars.add(jarPath); 718 paths.add(jarPath); 719 } 720 } 721 } 722 723 return paths.toArray(new String[paths.size()]); 724 } 725 726 @Override 727 public boolean canUpdateClasspathContainer(IPath containerPath, IJavaProject project) { 728 return true; 729 } 730 731 @Override 732 public void requestClasspathContainerUpdate(IPath containerPath, IJavaProject project, 733 IClasspathContainer containerSuggestion) throws CoreException { 734 AdtPlugin plugin = AdtPlugin.getDefault(); 735 736 synchronized (Sdk.getLock()) { 737 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 738 739 // check if the project has a valid target. 740 IAndroidTarget target = null; 741 if (sdkIsLoaded) { 742 target = Sdk.getCurrent().getTarget(project.getProject()); 743 } 744 if (sdkIsLoaded && target != null) { 745 String[] paths = getTargetPaths(target); 746 IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); 747 IClasspathEntry[] entries = containerSuggestion.getClasspathEntries(); 748 for (int i = 0; i < entries.length; i++) { 749 IClasspathEntry entry = entries[i]; 750 if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { 751 IPath entryPath = entry.getPath(); 752 753 if (entryPath != null) { 754 if (entryPath.equals(android_lib)) { 755 IPath entrySrcPath = entry.getSourceAttachmentPath(); 756 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); 757 if (entrySrcPath != null) { 758 ProjectHelper.saveStringProperty(root, 759 getAndroidSourceProperty(target), 760 entrySrcPath.toString()); 761 } else { 762 ProjectHelper.saveStringProperty(root, 763 getAndroidSourceProperty(target), null); 764 } 765 IClasspathAttribute[] extraAttributtes = entry.getExtraAttributes(); 766 if (extraAttributtes.length == 0) { 767 ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, 768 NULL_API_URL); 769 } 770 for (int j = 0; j < extraAttributtes.length; j++) { 771 IClasspathAttribute extraAttribute = extraAttributtes[j]; 772 String value = extraAttribute.getValue(); 773 if ((value == null || value.trim().length() == 0) 774 && IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME 775 .equals(extraAttribute.getName())) { 776 value = NULL_API_URL; 777 } 778 if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME 779 .equals(extraAttribute.getName())) { 780 ProjectHelper.saveStringProperty(root, 781 PROPERTY_ANDROID_API, value); 782 783 } 784 } 785 } 786 } 787 } 788 } 789 rebindClasspathEntries(project.getJavaModel(), containerPath); 790 } 791 } 792 } 793 794 private static void rebindClasspathEntries(IJavaModel model, IPath containerPath) 795 throws JavaModelException { 796 ArrayList<IJavaProject> affectedProjects = new ArrayList<IJavaProject>(); 797 798 IJavaProject[] projects = model.getJavaProjects(); 799 for (int i = 0; i < projects.length; i++) { 800 IJavaProject project = projects[i]; 801 IClasspathEntry[] entries = project.getRawClasspath(); 802 for (int k = 0; k < entries.length; k++) { 803 IClasspathEntry curr = entries[k]; 804 if (curr.getEntryKind() == IClasspathEntry.CPE_CONTAINER 805 && containerPath.equals(curr.getPath())) { 806 affectedProjects.add(project); 807 } 808 } 809 } 810 if (!affectedProjects.isEmpty()) { 811 IJavaProject[] affected = affectedProjects 812 .toArray(new IJavaProject[affectedProjects.size()]); 813 updateProjects(affected); 814 } 815 } 816 } 817