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.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.AndroidConstants; 21 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; 22 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 23 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 24 import com.android.sdklib.IAndroidTarget; 25 import com.android.sdklib.SdkConstants; 26 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 27 28 import org.eclipse.core.resources.IMarker; 29 import org.eclipse.core.resources.IProject; 30 import org.eclipse.core.resources.IResource; 31 import org.eclipse.core.runtime.CoreException; 32 import org.eclipse.core.runtime.IPath; 33 import org.eclipse.core.runtime.IProgressMonitor; 34 import org.eclipse.core.runtime.IStatus; 35 import org.eclipse.core.runtime.NullProgressMonitor; 36 import org.eclipse.core.runtime.Path; 37 import org.eclipse.core.runtime.Status; 38 import org.eclipse.core.runtime.jobs.Job; 39 import org.eclipse.jdt.core.ClasspathContainerInitializer; 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.IJavaProject; 45 import org.eclipse.jdt.core.JavaCore; 46 import org.eclipse.jdt.core.JavaModelException; 47 48 import java.io.File; 49 import java.net.URI; 50 import java.net.URISyntaxException; 51 import java.util.ArrayList; 52 import java.util.HashSet; 53 import java.util.regex.Pattern; 54 55 /** 56 * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to 57 * {@link IProject}s. This removes the hard-coded path to the android.jar. 58 */ 59 public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer { 60 /** The container id for the android framework jar file */ 61 private final static String CONTAINER_ID = 62 "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ 63 64 /** path separator to store multiple paths in a single property. This is guaranteed to not 65 * be in a path. 66 */ 67 private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ 68 69 private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ 70 private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ 71 private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ 72 private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; 73 74 private final static int CACHE_INDEX_JAR = 0; 75 private final static int CACHE_INDEX_SRC = 1; 76 private final static int CACHE_INDEX_DOCS_URI = 2; 77 private final static int CACHE_INDEX_OPT_DOCS_URI = 3; 78 private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; 79 AndroidClasspathContainerInitializer()80 public AndroidClasspathContainerInitializer() { 81 // pass 82 } 83 84 /** 85 * Binds a classpath container to a {@link IClasspathContainer} for a given project, 86 * or silently fails if unable to do so. 87 * @param containerPath the container path that is the container id. 88 * @param project the project to bind 89 */ 90 @Override initialize(IPath containerPath, IJavaProject project)91 public void initialize(IPath containerPath, IJavaProject project) throws CoreException { 92 if (CONTAINER_ID.equals(containerPath.toString())) { 93 JavaCore.setClasspathContainer(new Path(CONTAINER_ID), 94 new IJavaProject[] { project }, 95 new IClasspathContainer[] { allocateAndroidContainer(project) }, 96 new NullProgressMonitor()); 97 } 98 } 99 100 /** 101 * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER} 102 * linking to the Android Framework. 103 */ getContainerEntry()104 public static IClasspathEntry getContainerEntry() { 105 return JavaCore.newContainerEntry(new Path(CONTAINER_ID)); 106 } 107 108 /** 109 * Checks the {@link IPath} objects against the android framework container id and 110 * returns <code>true</code> if they are identical. 111 * @param path the <code>IPath</code> to check. 112 */ checkPath(IPath path)113 public static boolean checkPath(IPath path) { 114 return CONTAINER_ID.equals(path.toString()); 115 } 116 117 /** 118 * Updates the {@link IJavaProject} objects with new android framework container. This forces 119 * JDT to recompile them. 120 * @param androidProjects the projects to update. 121 * @return <code>true</code> if success, <code>false</code> otherwise. 122 */ updateProjects(IJavaProject[] androidProjects)123 public static boolean updateProjects(IJavaProject[] androidProjects) { 124 try { 125 // Allocate a new AndroidClasspathContainer, and associate it to the android framework 126 // container id for each projects. 127 // By providing a new association between a container id and a IClasspathContainer, 128 // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with 129 // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of 130 // the projects. 131 int projectCount = androidProjects.length; 132 133 IClasspathContainer[] containers = new IClasspathContainer[projectCount]; 134 for (int i = 0 ; i < projectCount; i++) { 135 containers[i] = allocateAndroidContainer(androidProjects[i]); 136 } 137 138 // give each project their new container in one call. 139 JavaCore.setClasspathContainer( 140 new Path(CONTAINER_ID), 141 androidProjects, containers, new NullProgressMonitor()); 142 143 return true; 144 } catch (JavaModelException e) { 145 return false; 146 } 147 } 148 149 /** 150 * Allocates and returns an {@link AndroidClasspathContainer} object with the proper 151 * path to the framework jar file. 152 * @param javaProject The java project that will receive the container. 153 */ allocateAndroidContainer(IJavaProject javaProject)154 private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { 155 final IProject iProject = javaProject.getProject(); 156 157 String markerMessage = null; 158 boolean outputToConsole = true; 159 160 try { 161 AdtPlugin plugin = AdtPlugin.getDefault(); 162 163 synchronized (Sdk.getLock()) { 164 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 165 166 // check if the project has a valid target. 167 ProjectState state = Sdk.getProjectState(iProject); 168 if (state == null) { 169 // looks like the project state (default.properties) couldn't be read! 170 markerMessage = String.format( 171 "Project has no default.properties file! Edit the project properties to set one."); 172 } else { 173 // this might be null if the sdk is not yet loaded. 174 IAndroidTarget target = state.getTarget(); 175 176 // if we are loaded and the target is non null, we create a valid 177 // ClassPathContainer 178 if (sdkIsLoaded && target != null) { 179 // first make sure the target has loaded its data 180 Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/); 181 182 // now do a quick check to make sure the project's target is compatible 183 // with library (if applicable). 184 if (state.hasLibraries() && 185 target.getProperty( 186 SdkConstants.PROP_SDK_SUPPORT_LIBRARY, false) == false) { 187 AdtPlugin.printErrorToConsole(iProject, String.format( 188 "Target '%1$s' does not support building project with libraries.", 189 target.getFullName())); 190 } 191 192 String targetName = target.getClasspathName(); 193 194 return new AndroidClasspathContainer( 195 createClasspathEntries(iProject, target, targetName), 196 new Path(CONTAINER_ID), targetName); 197 } 198 199 // In case of error, we'll try different thing to provide the best error message 200 // possible. 201 // Get the project's target's hash string (if it exists) 202 String hashString = state.getTargetHashString(); 203 204 if (hashString == null || hashString.length() == 0) { 205 // if there is no hash string we only show this if the SDK is loaded. 206 // For a project opened at start-up with no target, this would be displayed 207 // twice, once when the project is opened, and once after the SDK has 208 // finished loading. 209 // By testing the sdk is loaded, we only show this once in the console. 210 if (sdkIsLoaded) { 211 markerMessage = String.format( 212 "Project has no target set. Edit the project properties to set one."); 213 } 214 } else if (sdkIsLoaded) { 215 markerMessage = String.format( 216 "Unable to resolve target '%s'", hashString); 217 } else { 218 // this is the case where there is a hashString but the SDK is not yet 219 // loaded and therefore we can't get the target yet. 220 // We check if there is a cache of the needed information. 221 AndroidClasspathContainer container = getContainerFromCache(iProject); 222 223 if (container == null) { 224 // either the cache was wrong (ie folder does not exists anymore), or 225 // there was no cache. In this case we need to make sure the project 226 // is resolved again after the SDK is loaded. 227 plugin.setProjectToResolve(javaProject); 228 229 markerMessage = String.format( 230 "Unable to resolve target '%s' until the SDK is loaded.", 231 hashString); 232 233 // let's not log this one to the console as it will happen at 234 // every boot, and it's expected. (we do keep the error marker though). 235 outputToConsole = false; 236 237 } else { 238 // we created a container from the cache, so we register the project 239 // to be checked for cache validity once the SDK is loaded 240 plugin.setProjectToCheck(javaProject); 241 242 // and return the container 243 return container; 244 } 245 } 246 } 247 248 // return a dummy container to replace the one we may have had before. 249 // It'll be replaced by the real when if/when the target is resolved if/when the 250 // SDK finishes loading. 251 return new IClasspathContainer() { 252 public IClasspathEntry[] getClasspathEntries() { 253 return new IClasspathEntry[0]; 254 } 255 256 public String getDescription() { 257 return "Unable to get system library for the project"; 258 } 259 260 public int getKind() { 261 return IClasspathContainer.K_DEFAULT_SYSTEM; 262 } 263 264 public IPath getPath() { 265 return null; 266 } 267 }; 268 } 269 } finally { 270 if (markerMessage != null) { 271 // log the error and put the marker on the project if we can. 272 if (outputToConsole) { 273 AdtPlugin.printErrorToConsole(iProject, markerMessage); 274 } 275 276 try { 277 BaseProjectHelper.markProject(iProject, AndroidConstants.MARKER_TARGET, 278 markerMessage, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); 279 } catch (CoreException e) { 280 // In some cases, the workspace may be locked for modification when we 281 // pass here. 282 // We schedule a new job to put the marker after. 283 final String fmessage = markerMessage; 284 Job markerJob = new Job("Android SDK: Resolving error markers") { 285 @Override 286 protected IStatus run(IProgressMonitor monitor) { 287 try { 288 BaseProjectHelper.markProject(iProject, 289 AndroidConstants.MARKER_TARGET, 290 fmessage, IMarker.SEVERITY_ERROR, 291 IMarker.PRIORITY_HIGH); 292 } catch (CoreException e2) { 293 return e2.getStatus(); 294 } 295 296 return Status.OK_STATUS; 297 } 298 }; 299 300 // build jobs are run after other interactive jobs 301 markerJob.setPriority(Job.BUILD); 302 markerJob.schedule(); 303 } 304 } else { 305 // no error, remove potential MARKER_TARGETs. 306 try { 307 if (iProject.exists()) { 308 iProject.deleteMarkers(AndroidConstants.MARKER_TARGET, true, 309 IResource.DEPTH_INFINITE); 310 } 311 } catch (CoreException ce) { 312 // In some cases, the workspace may be locked for modification when we pass 313 // here, so we schedule a new job to put the marker after. 314 Job markerJob = new Job("Android SDK: Resolving error markers") { 315 @Override 316 protected IStatus run(IProgressMonitor monitor) { 317 try { 318 iProject.deleteMarkers(AndroidConstants.MARKER_TARGET, true, 319 IResource.DEPTH_INFINITE); 320 } catch (CoreException e2) { 321 return e2.getStatus(); 322 } 323 324 return Status.OK_STATUS; 325 } 326 }; 327 328 // build jobs are run after other interactive jobs 329 markerJob.setPriority(Job.BUILD); 330 markerJob.schedule(); 331 } 332 } 333 } 334 } 335 336 /** 337 * Creates and returns an array of {@link IClasspathEntry} objects for the android 338 * framework and optional libraries. 339 * <p/>This references the OS path to the android.jar and the 340 * java doc directory. This is dynamically created when a project is opened, 341 * and never saved in the project itself, so there's no risk of storing an 342 * obsolete path. 343 * The method also stores the paths used to create the entries in the project persistent 344 * properties. A new {@link AndroidClasspathContainer} can be created from the stored path 345 * using the {@link #getContainerFromCache(IProject)} method. 346 * @param project 347 * @param target The target that contains the libraries. 348 * @param targetName 349 */ 350 private static IClasspathEntry[] createClasspathEntries(IProject project, 351 IAndroidTarget target, String targetName) { 352 353 // get the path from the target 354 String[] paths = getTargetPaths(target); 355 356 // create the classpath entry from the paths 357 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); 358 359 // paths now contains all the path required to recreate the IClasspathEntry with no 360 // target info. We encode them in a single string, with each path separated by 361 // OS path separator. 362 StringBuilder sb = new StringBuilder(CACHE_VERSION); 363 for (String p : paths) { 364 sb.append(PATH_SEPARATOR); 365 sb.append(p); 366 } 367 368 // store this in a project persistent property 369 ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); 370 ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); 371 372 return entries; 373 } 374 375 /** 376 * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. 377 */ 378 private static AndroidClasspathContainer getContainerFromCache(IProject project) { 379 // get the cached info from the project persistent properties. 380 String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); 381 String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); 382 if (cache == null || targetNameCache == null) { 383 return null; 384 } 385 386 // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. 387 if (cache.startsWith(CACHE_VERSION_SEP) == false) { 388 return null; 389 } 390 391 cache = cache.substring(CACHE_VERSION_SEP.length()); 392 393 // the cache contains multiple paths, separated by a character guaranteed to not be in 394 // the path (\u001C). 395 // The first 3 are for android.jar (jar, source, doc), the rest are for the optional 396 // libraries and should contain at least one doc and a jar (if there are any libraries). 397 // Therefore, the path count should be 3 or 5+ 398 String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); 399 if (paths.length < 3 || paths.length == 4) { 400 return null; 401 } 402 403 // now we check the paths actually exist. 404 // There's an exception: If the source folder for android.jar does not exist, this is 405 // not a problem, so we skip it. 406 // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a 407 // bit differently. 408 try { 409 if (new File(paths[CACHE_INDEX_JAR]).exists() == false || 410 new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { 411 return null; 412 } 413 414 // check the path for the add-ons, if they exist. 415 if (paths.length > CACHE_INDEX_ADD_ON_START) { 416 417 // check the docs path separately from the rest of the paths as it's a URI. 418 if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { 419 return null; 420 } 421 422 // now just check the remaining paths. 423 for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { 424 String path = paths[i]; 425 if (path.length() > 0) { 426 File f = new File(path); 427 if (f.exists() == false) { 428 return null; 429 } 430 } 431 } 432 } 433 } catch (URISyntaxException e) { 434 return null; 435 } 436 437 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); 438 439 return new AndroidClasspathContainer(entries, 440 new Path(CONTAINER_ID), targetNameCache); 441 } 442 443 /** 444 * Generates an array of {@link IClasspathEntry} from a set of paths. 445 * @see #getTargetPaths(IAndroidTarget) 446 */ 447 private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) { 448 ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>(); 449 450 // First, we create the IClasspathEntry for the framework. 451 // now add the android framework to the class path. 452 // create the path object. 453 IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); 454 IPath android_src = new Path(paths[CACHE_INDEX_SRC]); 455 456 // create the java doc link. 457 IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( 458 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 459 paths[CACHE_INDEX_DOCS_URI]); 460 461 // create the access rule to restrict access to classes in com.android.internal 462 IAccessRule accessRule = JavaCore.newAccessRule( 463 new Path("com/android/internal/**"), //$NON-NLS-1$ 464 IAccessRule.K_NON_ACCESSIBLE); 465 466 IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib, 467 android_src, // source attachment path 468 null, // default source attachment root path. 469 new IAccessRule[] { accessRule }, 470 new IClasspathAttribute[] { cpAttribute }, 471 false // not exported. 472 ); 473 474 list.add(frameworkClasspathEntry); 475 476 // now deal with optional libraries 477 if (paths.length >= 5) { 478 String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; 479 int i = 4; 480 while (i < paths.length) { 481 Path jarPath = new Path(paths[i++]); 482 483 IClasspathAttribute[] attributes = null; 484 if (docPath.length() > 0) { 485 attributes = new IClasspathAttribute[] { 486 JavaCore.newClasspathAttribute( 487 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 488 docPath) 489 }; 490 } 491 492 IClasspathEntry entry = JavaCore.newLibraryEntry( 493 jarPath, 494 null, // source attachment path 495 null, // default source attachment root path. 496 null, 497 attributes, 498 false // not exported. 499 ); 500 list.add(entry); 501 } 502 } 503 504 return list.toArray(new IClasspathEntry[list.size()]); 505 } 506 507 /** 508 * Checks the projects' caches. If the cache was valid, the project is removed from the list. 509 * @param projects the list of projects to check. 510 */ 511 public static void checkProjectsCache(ArrayList<IJavaProject> projects) { 512 Sdk currentSdk = Sdk.getCurrent(); 513 int i = 0; 514 projectLoop: while (i < projects.size()) { 515 IJavaProject javaProject = projects.get(i); 516 IProject iProject = javaProject.getProject(); 517 518 // check if the project is opened 519 if (iProject.isOpen() == false) { 520 // remove from the list 521 // we do not increment i in this case. 522 projects.remove(i); 523 524 continue; 525 } 526 527 // project that have been resolved before the sdk was loaded 528 // will have a ProjectState where the IAndroidTarget is null 529 // so we load the target now that the SDK is loaded. 530 IAndroidTarget target = currentSdk.loadTarget(Sdk.getProjectState(iProject)); 531 if (target == null) { 532 // this is really not supposed to happen. This would mean there are cached paths, 533 // but default.properties was deleted. Keep the project in the list to force 534 // a resolve which will display the error. 535 i++; 536 continue; 537 } 538 539 String[] targetPaths = getTargetPaths(target); 540 541 // now get the cached paths 542 String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); 543 if (cache == null) { 544 // this should not happen. We'll force resolve again anyway. 545 i++; 546 continue; 547 } 548 549 String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); 550 if (cachedPaths.length < 3 || cachedPaths.length == 4) { 551 // paths length is wrong. simply resolve the project again 552 i++; 553 continue; 554 } 555 556 // Now we compare the paths. The first 4 can be compared directly. 557 // because of case sensitiveness we need to use File objects 558 559 if (targetPaths.length != cachedPaths.length) { 560 // different paths, force resolve again. 561 i++; 562 continue; 563 } 564 565 // compare the main paths (android.jar, main sources, main javadoc) 566 if (new File(targetPaths[CACHE_INDEX_JAR]).equals( 567 new File(cachedPaths[CACHE_INDEX_JAR])) == false || 568 new File(targetPaths[CACHE_INDEX_SRC]).equals( 569 new File(cachedPaths[CACHE_INDEX_SRC])) == false || 570 new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( 571 new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { 572 // different paths, force resolve again. 573 i++; 574 continue; 575 } 576 577 if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { 578 // compare optional libraries javadoc 579 if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( 580 new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { 581 // different paths, force resolve again. 582 i++; 583 continue; 584 } 585 586 // testing the optional jar files is a little bit trickier. 587 // The order is not guaranteed to be identical. 588 // From a previous test, we do know however that there is the same number. 589 // The number of libraries should be low enough that we can simply go through the 590 // lists manually. 591 targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { 592 String targetPath = targetPaths[tpi]; 593 594 // look for a match in the other array 595 for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { 596 if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { 597 // found a match. Try the next targetPath 598 continue targetLoop; 599 } 600 } 601 602 // if we stop here, we haven't found a match, which means there's a 603 // discrepancy in the libraries. We force a resolve. 604 i++; 605 continue projectLoop; 606 } 607 } 608 609 // at the point the check passes, and we can remove the project from the list. 610 // we do not increment i in this case. 611 projects.remove(i); 612 } 613 } 614 615 /** 616 * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. 617 * <p/>The paths are always in the same order. 618 * <ul> 619 * <li>Path to android.jar</li> 620 * <li>Path to the source code for android.jar</li> 621 * <li>Path to the javadoc for the android platform</li> 622 * </ul> 623 * Additionally, if there are optional libraries, the array will contain: 624 * <ul> 625 * <li>Path to the libraries javadoc</li> 626 * <li>Path to the first .jar file</li> 627 * <li>(more .jar as needed)</li> 628 * </ul> 629 */ 630 private static String[] getTargetPaths(IAndroidTarget target) { 631 ArrayList<String> paths = new ArrayList<String>(); 632 633 // first, we get the path for android.jar 634 // The order is: android.jar, source folder, docs folder 635 paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); 636 paths.add(target.getPath(IAndroidTarget.SOURCES)); 637 paths.add(AdtPlugin.getUrlDoc()); 638 639 // now deal with optional libraries. 640 IOptionalLibrary[] libraries = target.getOptionalLibraries(); 641 if (libraries != null) { 642 // all the optional libraries use the same javadoc, so we start with this 643 String targetDocPath = target.getPath(IAndroidTarget.DOCS); 644 if (targetDocPath != null) { 645 paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); 646 } else { 647 // we add an empty string, to always have the same count. 648 paths.add(""); 649 } 650 651 // because different libraries could use the same jar file, we make sure we add 652 // each jar file only once. 653 HashSet<String> visitedJars = new HashSet<String>(); 654 for (IOptionalLibrary library : libraries) { 655 String jarPath = library.getJarPath(); 656 if (visitedJars.contains(jarPath) == false) { 657 visitedJars.add(jarPath); 658 paths.add(jarPath); 659 } 660 } 661 } 662 663 return paths.toArray(new String[paths.size()]); 664 } 665 } 666