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