Home | History | Annotate | Download | only in project
      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