Home | History | Annotate | Download | only in sdk
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.sdk;
     18 
     19 import com.android.ddmlib.IDevice;
     20 import com.android.ide.eclipse.adt.AdtPlugin;
     21 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer;
     22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     23 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
     24 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
     25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
     26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
     27 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
     28 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
     29 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
     30 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
     31 import com.android.prefs.AndroidLocation.AndroidLocationException;
     32 import com.android.sdklib.AndroidVersion;
     33 import com.android.sdklib.IAndroidTarget;
     34 import com.android.sdklib.ISdkLog;
     35 import com.android.sdklib.SdkConstants;
     36 import com.android.sdklib.SdkManager;
     37 import com.android.sdklib.internal.avd.AvdManager;
     38 import com.android.sdklib.internal.project.ProjectProperties;
     39 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
     40 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
     41 import com.android.sdklib.io.StreamException;
     42 
     43 import org.eclipse.core.resources.IFile;
     44 import org.eclipse.core.resources.IFolder;
     45 import org.eclipse.core.resources.IMarkerDelta;
     46 import org.eclipse.core.resources.IPathVariableManager;
     47 import org.eclipse.core.resources.IProject;
     48 import org.eclipse.core.resources.IProjectDescription;
     49 import org.eclipse.core.resources.IResource;
     50 import org.eclipse.core.resources.IResourceDelta;
     51 import org.eclipse.core.resources.IWorkspaceRoot;
     52 import org.eclipse.core.resources.IncrementalProjectBuilder;
     53 import org.eclipse.core.resources.ResourcesPlugin;
     54 import org.eclipse.core.runtime.CoreException;
     55 import org.eclipse.core.runtime.IPath;
     56 import org.eclipse.core.runtime.IProgressMonitor;
     57 import org.eclipse.core.runtime.IStatus;
     58 import org.eclipse.core.runtime.Path;
     59 import org.eclipse.core.runtime.Status;
     60 import org.eclipse.core.runtime.jobs.Job;
     61 import org.eclipse.jdt.core.IClasspathEntry;
     62 import org.eclipse.jdt.core.IJavaProject;
     63 import org.eclipse.jdt.core.JavaCore;
     64 
     65 import java.io.File;
     66 import java.io.IOException;
     67 import java.net.MalformedURLException;
     68 import java.net.URL;
     69 import java.util.ArrayList;
     70 import java.util.Arrays;
     71 import java.util.HashMap;
     72 import java.util.HashSet;
     73 import java.util.List;
     74 import java.util.Map;
     75 import java.util.Set;
     76 import java.util.Map.Entry;
     77 
     78 /**
     79  * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
     80  * at the same time.
     81  *
     82  * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
     83  * the Sdk object.
     84  *
     85  * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
     86  */
     87 public final class Sdk  {
     88     private static final String PROP_LIBRARY = "_library"; //$NON-NLS-1$
     89     private static final String PROP_LIBRARY_NAME = "_library_name"; //$NON-NLS-1$
     90     public static final String CREATOR_ADT = "ADT";        //$NON-NLS-1$
     91     public static final String PROP_CREATOR = "_creator";  //$NON-NLS-1$
     92     private final static Object sLock = new Object();
     93 
     94     private static Sdk sCurrentSdk = null;
     95 
     96     /**
     97      * Map associating {@link IProject} and their state {@link ProjectState}.
     98      * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
     99      */
    100     private final static HashMap<IProject, ProjectState> sProjectStateMap =
    101             new HashMap<IProject, ProjectState>();
    102 
    103     /**
    104      * Data bundled using during the load of Target data.
    105      * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
    106      * to compile before the loading was finished. Those projects will be recompiled
    107      * at the end of the loading.
    108      */
    109     private final static class TargetLoadBundle {
    110         LoadStatus status;
    111         final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>();
    112     }
    113 
    114     private final SdkManager mManager;
    115     private final AvdManager mAvdManager;
    116 
    117     /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
    118     private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
    119         new HashMap<IAndroidTarget, AndroidTargetData>();
    120     /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
    121     private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
    122         new HashMap<IAndroidTarget, TargetLoadBundle>();
    123 
    124     private final String mDocBaseUrl;
    125 
    126     private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager();
    127 
    128     /**
    129      * Classes implementing this interface will receive notification when targets are changed.
    130      */
    131     public interface ITargetChangeListener {
    132         /**
    133          * Sent when project has its target changed.
    134          */
    135         void onProjectTargetChange(IProject changedProject);
    136 
    137         /**
    138          * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
    139          * or the SDK is changed).
    140          */
    141         void onTargetLoaded(IAndroidTarget target);
    142 
    143         /**
    144          * Called when the base content of the SDK is parsed.
    145          */
    146         void onSdkLoaded();
    147     }
    148 
    149     /**
    150      * Basic abstract implementation of the ITargetChangeListener for the case where both
    151      * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
    152      * use the same code based on a simple test requiring to know the current IProject.
    153      */
    154     public static abstract class TargetChangeListener implements ITargetChangeListener {
    155         /**
    156          * Returns the {@link IProject} associated with the listener.
    157          */
    158         public abstract IProject getProject();
    159 
    160         /**
    161          * Called when the listener needs to take action on the event. This is only called
    162          * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
    163          * match the values received in {@link #onProjectTargetChange(IProject)} and
    164          * {@link #onTargetLoaded(IAndroidTarget)}.
    165          */
    166         public abstract void reload();
    167 
    168         public void onProjectTargetChange(IProject changedProject) {
    169             if (changedProject != null && changedProject.equals(getProject())) {
    170                 reload();
    171             }
    172         }
    173 
    174         public void onTargetLoaded(IAndroidTarget target) {
    175             IProject project = getProject();
    176             if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
    177                 reload();
    178             }
    179         }
    180 
    181         public void onSdkLoaded() {
    182             // do nothing;
    183         }
    184     }
    185 
    186     /**
    187      * Returns the lock object used to synchronize all operations dealing with SDK, targets and
    188      * projects.
    189      */
    190     public static final Object getLock() {
    191         return sLock;
    192     }
    193 
    194     /**
    195      * Loads an SDK and returns an {@link Sdk} object if success.
    196      * <p/>If the SDK failed to load, it displays an error to the user.
    197      * @param sdkLocation the OS path to the SDK.
    198      */
    199     public static Sdk loadSdk(String sdkLocation) {
    200         synchronized (sLock) {
    201             if (sCurrentSdk != null) {
    202                 sCurrentSdk.dispose();
    203                 sCurrentSdk = null;
    204             }
    205 
    206             final ArrayList<String> logMessages = new ArrayList<String>();
    207             ISdkLog log = new ISdkLog() {
    208                 public void error(Throwable throwable, String errorFormat, Object... arg) {
    209                     if (errorFormat != null) {
    210                         logMessages.add(String.format("Error: " + errorFormat, arg));
    211                     }
    212 
    213                     if (throwable != null) {
    214                         logMessages.add(throwable.getMessage());
    215                     }
    216                 }
    217 
    218                 public void warning(String warningFormat, Object... arg) {
    219                     logMessages.add(String.format("Warning: " + warningFormat, arg));
    220                 }
    221 
    222                 public void printf(String msgFormat, Object... arg) {
    223                     logMessages.add(String.format(msgFormat, arg));
    224                 }
    225             };
    226 
    227             // get an SdkManager object for the location
    228             SdkManager manager = SdkManager.createManager(sdkLocation, log);
    229             if (manager != null) {
    230                 AvdManager avdManager = null;
    231                 try {
    232                     avdManager = new AvdManager(manager, log);
    233                 } catch (AndroidLocationException e) {
    234                     log.error(e, "Error parsing the AVDs");
    235                 }
    236                 sCurrentSdk = new Sdk(manager, avdManager);
    237                 return sCurrentSdk;
    238             } else {
    239                 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n");
    240                 for (String msg : logMessages) {
    241                     sb.append('\n');
    242                     sb.append(msg);
    243                 }
    244                 AdtPlugin.displayError("Android SDK", sb.toString());
    245             }
    246             return null;
    247         }
    248     }
    249 
    250     /**
    251      * Returns the current {@link Sdk} object.
    252      */
    253     public static Sdk getCurrent() {
    254         synchronized (sLock) {
    255             return sCurrentSdk;
    256         }
    257     }
    258 
    259     /**
    260      * Returns the location (OS path) of the current SDK.
    261      */
    262     public String getSdkLocation() {
    263         return mManager.getLocation();
    264     }
    265 
    266     /**
    267      * Returns the URL to the local documentation.
    268      * Can return null if no documentation is found in the current SDK.
    269      *
    270      * @return A file:// URL on the local documentation folder if it exists or null.
    271      */
    272     public String getDocumentationBaseUrl() {
    273         return mDocBaseUrl;
    274     }
    275 
    276     /**
    277      * Returns the list of targets that are available in the SDK.
    278      */
    279     public IAndroidTarget[] getTargets() {
    280         return mManager.getTargets();
    281     }
    282 
    283     /**
    284      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
    285      *
    286      * @param hash the {@link IAndroidTarget} hash string.
    287      * @return The matching {@link IAndroidTarget} or null.
    288      */
    289     public IAndroidTarget getTargetFromHashString(String hash) {
    290         return mManager.getTargetFromHashString(hash);
    291     }
    292 
    293     /**
    294      * Initializes a new project with a target. This creates the <code>default.properties</code>
    295      * file.
    296      * @param project the project to intialize
    297      * @param target the project's target.
    298      * @throws IOException if creating the file failed in any way.
    299      * @throws StreamException
    300      */
    301     public void initProject(IProject project, IAndroidTarget target)
    302             throws IOException, StreamException {
    303         if (project == null || target == null) {
    304             return;
    305         }
    306 
    307         synchronized (sLock) {
    308             // check if there's already a state?
    309             ProjectState state = getProjectState(project);
    310 
    311             ProjectPropertiesWorkingCopy properties = null;
    312 
    313             if (state != null) {
    314                 properties = state.getProperties().makeWorkingCopy();
    315             }
    316 
    317             if (properties == null) {
    318                 IPath location = project.getLocation();
    319                 if (location == null) {  // can return null when the project is being deleted.
    320                     // do nothing and return null;
    321                     return;
    322                 }
    323 
    324                 properties = ProjectProperties.create(location.toOSString(), PropertyType.DEFAULT);
    325             }
    326 
    327             // save the target hash string in the project persistent property
    328             properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
    329             properties.save();
    330         }
    331     }
    332 
    333     /**
    334      * Returns the {@link ProjectState} object associated with a given project.
    335      * <p/>
    336      * This method is the only way to properly get the project's {@link ProjectState}
    337      * If the project has not yet been loaded, then it is loaded.
    338      * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
    339      * objects, and therefore is static.
    340      * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
    341      * are replaced.
    342      * @param project the request project
    343      * @return the ProjectState for the project.
    344      */
    345     public static ProjectState getProjectState(IProject project) {
    346         if (project == null) {
    347             return null;
    348         }
    349 
    350         synchronized (sLock) {
    351             ProjectState state = sProjectStateMap.get(project);
    352             if (state == null) {
    353                 // load the default.properties from the project folder.
    354                 IPath location = project.getLocation();
    355                 if (location == null) {  // can return null when the project is being deleted.
    356                     // do nothing and return null;
    357                     return null;
    358                 }
    359 
    360                 ProjectProperties properties = ProjectProperties.load(location.toOSString(),
    361                         PropertyType.DEFAULT);
    362                 if (properties == null) {
    363                     AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'",
    364                             project.getName());
    365                     return null;
    366                 }
    367 
    368                 state = new ProjectState(project, properties);
    369                 sProjectStateMap.put(project, state);
    370 
    371                 // try to resolve the target
    372                 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
    373                     sCurrentSdk.loadTarget(state);
    374                 }
    375             }
    376 
    377             return state;
    378         }
    379     }
    380 
    381     /**
    382      * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
    383      */
    384     public IAndroidTarget getTarget(IProject project) {
    385         if (project == null) {
    386             return null;
    387         }
    388 
    389         ProjectState state = getProjectState(project);
    390         if (state != null) {
    391             return state.getTarget();
    392         }
    393 
    394         return null;
    395     }
    396 
    397     /**
    398      * Loads the {@link IAndroidTarget} for a given project.
    399      * <p/>This method will get the target hash string from the project properties, and resolve
    400      * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
    401      * @param state the state representing the project to load.
    402      * @return the target that was loaded.
    403      */
    404     public IAndroidTarget loadTarget(ProjectState state) {
    405         IAndroidTarget target = null;
    406         String hash = state.getTargetHashString();
    407         if (hash != null) {
    408             state.setTarget(target = getTargetFromHashString(hash));
    409         }
    410 
    411         return target;
    412     }
    413 
    414     /**
    415      * Checks and loads (if needed) the data for a given target.
    416      * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
    417      * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
    418      * <p/>An optional project as second parameter can be given to be recompiled once the target
    419      * data is finished loading.
    420      * <p/>The return value is non-null only if the target data has already been loaded (and in this
    421      * case is the status of the load operation)
    422      * @param target the target to load.
    423      * @param project an optional project to be recompiled when the target data is loaded.
    424      * If the target is already loaded, nothing happens.
    425      * @return The load status if the target data is already loaded.
    426      */
    427     public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
    428         boolean loadData = false;
    429 
    430         synchronized (sLock) {
    431             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    432             if (bundle == null) {
    433                 bundle = new TargetLoadBundle();
    434                 mTargetDataStatusMap.put(target,bundle);
    435 
    436                 // set status to loading
    437                 bundle.status = LoadStatus.LOADING;
    438 
    439                 // add project to bundle
    440                 if (project != null) {
    441                     bundle.projecsToReload.add(project);
    442                 }
    443 
    444                 // and set the flag to start the loading below
    445                 loadData = true;
    446             } else if (bundle.status == LoadStatus.LOADING) {
    447                 // add project to bundle
    448                 if (project != null) {
    449                     bundle.projecsToReload.add(project);
    450                 }
    451 
    452                 return bundle.status;
    453             } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
    454                 return bundle.status;
    455             }
    456         }
    457 
    458         if (loadData) {
    459             Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
    460                 @Override
    461                 protected IStatus run(IProgressMonitor monitor) {
    462                     AdtPlugin plugin = AdtPlugin.getDefault();
    463                     try {
    464                         IStatus status = new AndroidTargetParser(target).run(monitor);
    465 
    466                         IJavaProject[] javaProjectArray = null;
    467 
    468                         synchronized (sLock) {
    469                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    470 
    471                             if (status.getCode() != IStatus.OK) {
    472                                 bundle.status = LoadStatus.FAILED;
    473                                 bundle.projecsToReload.clear();
    474                             } else {
    475                                 bundle.status = LoadStatus.LOADED;
    476 
    477                                 // Prepare the array of project to recompile.
    478                                 // The call is done outside of the synchronized block.
    479                                 javaProjectArray = bundle.projecsToReload.toArray(
    480                                         new IJavaProject[bundle.projecsToReload.size()]);
    481 
    482                                 // and update the UI of the editors that depend on the target data.
    483                                 plugin.updateTargetListeners(target);
    484                             }
    485                         }
    486 
    487                         if (javaProjectArray != null) {
    488                             AndroidClasspathContainerInitializer.updateProjects(javaProjectArray);
    489                         }
    490 
    491                         return status;
    492                     } catch (Throwable t) {
    493                         synchronized (sLock) {
    494                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    495                             bundle.status = LoadStatus.FAILED;
    496                         }
    497 
    498                         AdtPlugin.log(t, "Exception in checkAndLoadTargetData.");    //$NON-NLS-1$
    499                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    500                                 String.format(
    501                                         "Parsing Data for %1$s failed", //$NON-NLS-1$
    502                                         target.hashString()),
    503                                 t);
    504                     }
    505                 }
    506             };
    507             job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
    508             job.schedule();
    509         }
    510 
    511         // The only way to go through here is when the loading starts through the Job.
    512         // Therefore the current status of the target is LOADING.
    513         return LoadStatus.LOADING;
    514     }
    515 
    516     /**
    517      * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
    518      */
    519     public AndroidTargetData getTargetData(IAndroidTarget target) {
    520         synchronized (sLock) {
    521             return mTargetDataMap.get(target);
    522         }
    523     }
    524 
    525     /**
    526      * Return the {@link AndroidTargetData} for a given {@link IProject}.
    527      */
    528     public AndroidTargetData getTargetData(IProject project) {
    529         synchronized (sLock) {
    530             IAndroidTarget target = getTarget(project);
    531             if (target != null) {
    532                 return getTargetData(target);
    533             }
    534         }
    535 
    536         return null;
    537     }
    538 
    539     /**
    540      * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
    541      * be <code>null</code>.
    542      */
    543     public AvdManager getAvdManager() {
    544         return mAvdManager;
    545     }
    546 
    547     public static AndroidVersion getDeviceVersion(IDevice device) {
    548         try {
    549             Map<String, String> props = device.getProperties();
    550             String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
    551             if (apiLevel == null) {
    552                 return null;
    553             }
    554 
    555             return new AndroidVersion(Integer.parseInt(apiLevel),
    556                     props.get((IDevice.PROP_BUILD_CODENAME)));
    557         } catch (NumberFormatException e) {
    558             return null;
    559         }
    560     }
    561 
    562     public LayoutDeviceManager getLayoutDeviceManager() {
    563         return mLayoutDeviceManager;
    564     }
    565 
    566     /**
    567      * Returns a list of {@link ProjectState} representing projects depending, directly or
    568      * indirectly on a given library project.
    569      * @param project the library project.
    570      * @return a possibly empty list of ProjectState.
    571      */
    572     public static Set<ProjectState> getMainProjectsFor(IProject project) {
    573         synchronized (sLock) {
    574             // first get the project directly depending on this.
    575             HashSet<ProjectState> list = new HashSet<ProjectState>();
    576 
    577             // loop on all project and see if ProjectState.getLibrary returns a non null
    578             // project.
    579             for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
    580                 if (project != entry.getKey()) {
    581                     LibraryState library = entry.getValue().getLibrary(project);
    582                     if (library != null) {
    583                         list.add(entry.getValue());
    584                     }
    585                 }
    586             }
    587 
    588             // now look for projects depending on the projects directly depending on the library.
    589             HashSet<ProjectState> result = new HashSet<ProjectState>(list);
    590             for (ProjectState p : list) {
    591                 if (p.isLibrary()) {
    592                     Set<ProjectState> set = getMainProjectsFor(p.getProject());
    593                     result.addAll(set);
    594                 }
    595             }
    596 
    597             return result;
    598         }
    599     }
    600 
    601     private Sdk(SdkManager manager, AvdManager avdManager) {
    602         mManager = manager;
    603         mAvdManager = avdManager;
    604 
    605         // listen to projects closing
    606         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    607         monitor.addProjectListener(mProjectListener);
    608         monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED);
    609         monitor.addResourceEventListener(mResourceEventListener);
    610 
    611         // pre-compute some paths
    612         mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() +
    613                 SdkConstants.OS_SDK_DOCS_FOLDER);
    614 
    615         // load the built-in and user layout devices
    616         mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation());
    617         // and the ones from the add-on
    618         loadLayoutDevices();
    619 
    620         // update whatever ProjectState is already present with new IAndroidTarget objects.
    621         synchronized (sLock) {
    622             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
    623                 entry.getValue().setTarget(
    624                         getTargetFromHashString(entry.getValue().getTargetHashString()));
    625             }
    626         }
    627     }
    628 
    629     /**
    630      *  Cleans and unloads the SDK.
    631      */
    632     private void dispose() {
    633         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    634         monitor.removeProjectListener(mProjectListener);
    635         monitor.removeFileListener(mFileListener);
    636         monitor.removeResourceEventListener(mResourceEventListener);
    637 
    638         // the IAndroidTarget objects are now obsolete so update the project states.
    639         synchronized (sLock) {
    640             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
    641                 entry.getValue().setTarget(null);
    642             }
    643         }
    644     }
    645 
    646     void setTargetData(IAndroidTarget target, AndroidTargetData data) {
    647         synchronized (sLock) {
    648             mTargetDataMap.put(target, data);
    649         }
    650     }
    651 
    652     /**
    653      * Returns the URL to the local documentation.
    654      * Can return null if no documentation is found in the current SDK.
    655      *
    656      * @param osDocsPath Path to the documentation folder in the current SDK.
    657      *  The folder may not actually exist.
    658      * @return A file:// URL on the local documentation folder if it exists or null.
    659      */
    660     private String getDocumentationBaseUrl(String osDocsPath) {
    661         File f = new File(osDocsPath);
    662 
    663         if (f.isDirectory()) {
    664             try {
    665                 // Note: to create a file:// URL, one would typically use something like
    666                 // f.toURI().toURL().toString(). However this generates a broken path on
    667                 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
    668                 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
    669                 // do the correct thing manually.
    670 
    671                 String path = f.getAbsolutePath();
    672                 if (File.separatorChar != '/') {
    673                     path = path.replace(File.separatorChar, '/');
    674                 }
    675 
    676                 // For some reason the URL class doesn't add the mandatory "//" after
    677                 // the "file:" protocol name, so it has to be hacked into the path.
    678                 URL url = new URL("file", null, "//" + path);  //$NON-NLS-1$ //$NON-NLS-2$
    679                 String result = url.toString();
    680                 return result;
    681             } catch (MalformedURLException e) {
    682                 // ignore malformed URLs
    683             }
    684         }
    685 
    686         return null;
    687     }
    688 
    689     /**
    690      * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to
    691      * load {@link LayoutDevice} from them.
    692      */
    693     private void loadLayoutDevices() {
    694         IAndroidTarget[] targets = mManager.getTargets();
    695         for (IAndroidTarget target : targets) {
    696             if (target.isPlatform() == false) {
    697                 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML);
    698                 if (deviceXml.isFile()) {
    699                     mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml);
    700                 }
    701             }
    702         }
    703 
    704         mLayoutDeviceManager.sealAddonLayoutDevices();
    705     }
    706 
    707     /**
    708      * Delegate listener for project changes.
    709      */
    710     private IProjectListener mProjectListener = new IProjectListener() {
    711         public void projectClosed(IProject project) {
    712             onProjectRemoved(project, false /*deleted*/);
    713         }
    714 
    715         public void projectDeleted(IProject project) {
    716             onProjectRemoved(project, true /*deleted*/);
    717         }
    718 
    719         private void onProjectRemoved(IProject project, boolean deleted) {
    720             // get the target project
    721             synchronized (sLock) {
    722                 // Don't use getProject() as it could create the ProjectState if it's not
    723                 // there yet and this is not what we want. We want the current object.
    724                 // Therefore, direct access to the map.
    725                 ProjectState state = sProjectStateMap.get(project);
    726                 if (state != null) {
    727                     // 1. clear the layout lib cache associated with this project
    728                     IAndroidTarget target = state.getTarget();
    729                     if (target != null) {
    730                         // get the bridge for the target, and clear the cache for this project.
    731                         AndroidTargetData data = mTargetDataMap.get(target);
    732                         if (data != null) {
    733                             LayoutBridge bridge = data.getLayoutBridge();
    734                             if (bridge != null && bridge.status == LoadStatus.LOADED) {
    735                                 bridge.bridge.clearCaches(project);
    736                             }
    737                         }
    738                     }
    739 
    740                     // 2. if the project is a library, make sure to update the
    741                     // LibraryState for any main project using this.
    742                     // Also, record the updated projects that are libraries, to update
    743                     // projects that depend on them.
    744                     ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
    745                     for (ProjectState projectState : sProjectStateMap.values()) {
    746                         LibraryState libState = projectState.getLibrary(project);
    747                         if (libState != null) {
    748                             // get the current libraries.
    749                             IProject[] oldLibraries = projectState.getFullLibraryProjects();
    750 
    751                             // the unlink below will work in the job, but we need to close
    752                             // the library right away.
    753                             // This is because in case of a rename of a project, projectClosed and
    754                             // projectOpened will be called before any other job is run, so we
    755                             // need to make sure projectOpened is closed with the main project
    756                             // state up to date.
    757                             libState.close();
    758 
    759 
    760                             // edit the project to remove the linked source folder.
    761                             // this also calls LibraryState.close();
    762                             LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
    763                             if (bundle != null) {
    764                                 queueLinkUpdateBundle(bundle);
    765                             }
    766 
    767                             if (projectState.isLibrary()) {
    768                                 updatedLibraries.add(projectState);
    769                             }
    770                         }
    771                     }
    772 
    773                     if (deleted) {
    774                         // remove the linked path variable
    775                         disposeLibraryProject(project);
    776                     }
    777 
    778                     // now remove the project for the project map.
    779                     sProjectStateMap.remove(project);
    780 
    781                     // update the projects that depend on the updated project
    782                     updateProjectsWithNewLibraries(updatedLibraries);
    783                 }
    784             }
    785         }
    786 
    787         public void projectOpened(IProject project) {
    788             onProjectOpened(project);
    789         }
    790 
    791         public void projectOpenedWithWorkspace(IProject project) {
    792             // no need to force recompilation when projects are opened with the workspace.
    793             onProjectOpened(project);
    794         }
    795 
    796         private void onProjectOpened(final IProject openedProject) {
    797             ProjectState openedState = getProjectState(openedProject);
    798             if (openedState != null) {
    799                 if (openedState.hasLibraries()) {
    800                     // list of library to link to the opened project.
    801                     final ArrayList<IProject> libsToLink = new ArrayList<IProject>();
    802 
    803                     // Look for all other opened projects to see if any is a library for the opened
    804                     // project.
    805                     synchronized (sLock) {
    806                         for (ProjectState projectState : sProjectStateMap.values()) {
    807                             if (projectState != openedState) {
    808                                 // ProjectState#needs() both checks if this is a missing library
    809                                 // and updates LibraryState to contains the new values.
    810                                 LibraryState libState = openedState.needs(projectState);
    811 
    812                                 if (libState != null) {
    813                                     // we have a match! Add the library to the list (if it was
    814                                     // not added through an indirect dependency before).
    815                                     IProject libProject = libState.getProjectState().getProject();
    816                                     if (libsToLink.contains(libProject) == false) {
    817                                         libsToLink.add(libProject);
    818                                     }
    819 
    820                                     // now find what this depends on, and add it too.
    821                                     // The order here doesn't matter
    822                                     // as it's just to add the linked source folder, so there's no
    823                                     // need to use ProjectState#getFullLibraryProjects() which
    824                                     // could return project that have already been added anyway.
    825                                     fillProjectDependenciesList(libState.getProjectState(),
    826                                             libsToLink);
    827                                 }
    828                             }
    829                         }
    830                     }
    831 
    832                     // create a link bundle always, because even if there's no libraries to add
    833                     // to the CPE, the cleaning of invalid CPE must happen.
    834                     LinkUpdateBundle bundle = new LinkUpdateBundle();
    835                     bundle.mProject = openedProject;
    836                     bundle.mNewLibraryProjects = libsToLink.toArray(
    837                             new IProject[libsToLink.size()]);
    838                     bundle.mCleanupCPE = true;
    839                     queueLinkUpdateBundle(bundle);
    840                 }
    841 
    842                 // if the project is a library, then add it to the list of projects being opened.
    843                 // They will be processed in IResourceEventListener#resourceChangeEventEnd.
    844                 // This is done so that we are sure to process all the projects being opened
    845                 // first and only then process projects depending on the projects that were opened.
    846                 if (openedState.isLibrary()) {
    847                     setupLibraryProject(openedProject);
    848 
    849                     mOpenedLibraryProjects.add(openedState);
    850                 }
    851             }
    852         }
    853 
    854         public void projectRenamed(IProject project, IPath from) {
    855             System.out.println("RENAMED: " + project);
    856             // a project was renamed.
    857             // if the project is a library, look for any project that depended on it
    858             // and update it. (default.properties and linked source folder)
    859             ProjectState renamedState = getProjectState(project);
    860             if (renamedState.isLibrary()) {
    861                 // remove the variable
    862                 disposeLibraryProject(from.lastSegment());
    863 
    864                 // update the project depending on the library
    865                 synchronized (sLock) {
    866                     for (ProjectState projectState : sProjectStateMap.values()) {
    867                         if (projectState != renamedState && projectState.isMissingLibraries()) {
    868                             IPath oldRelativePath = makeRelativeTo(from,
    869                                     projectState.getProject().getFullPath());
    870 
    871                             IPath newRelativePath = makeRelativeTo(project.getFullPath(),
    872                                     projectState.getProject().getFullPath());
    873 
    874                             // get the current libraries
    875                             IProject[] oldLibraries = projectState.getFullLibraryProjects();
    876 
    877                             // update the library for the main project.
    878                             LibraryState libState = projectState.updateLibrary(
    879                                     oldRelativePath.toString(), newRelativePath.toString(),
    880                                     renamedState);
    881                             if (libState != null) {
    882                                 // this project depended on the renamed library, create a bundle
    883                                 // with the whole library difference (in case the renamed library
    884                                 // also depends on libraries).
    885 
    886                                 LinkUpdateBundle bundle = getLinkBundle(projectState,
    887                                         oldLibraries);
    888                                 queueLinkUpdateBundle(bundle);
    889 
    890                                 // add it to the opened projects to update whatever depends
    891                                 // on it
    892                                 if (projectState.isLibrary()) {
    893                                     mOpenedLibraryProjects.add(projectState);
    894                                 }
    895                             }
    896                         }
    897                     }
    898                 }
    899             }
    900         }
    901     };
    902 
    903     /**
    904      * Delegate listener for file changes.
    905      */
    906     private IFileListener mFileListener = new IFileListener() {
    907         public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) {
    908             if (SdkConstants.FN_DEFAULT_PROPERTIES.equals(file.getName()) &&
    909                     file.getParent() == file.getProject()) {
    910                 try {
    911                     // reload the content of the default.properties file and update
    912                     // the target.
    913                     IProject iProject = file.getProject();
    914                     ProjectState state = Sdk.getProjectState(iProject);
    915 
    916                     // get the current target
    917                     IAndroidTarget oldTarget = state.getTarget();
    918 
    919                     // get the current library flag
    920                     boolean wasLibrary = state.isLibrary();
    921 
    922                     // get the current list of project dependencies
    923                     IProject[] oldLibraries = state.getFullLibraryProjects();
    924 
    925                     LibraryDifference diff = state.reloadProperties();
    926 
    927                     // load the (possibly new) target.
    928                     IAndroidTarget newTarget = loadTarget(state);
    929 
    930                     // check if this is a new library
    931                     if (state.isLibrary() && wasLibrary == false) {
    932                         setupLibraryProject(iProject);
    933                     }
    934 
    935                     // reload the libraries if needed
    936                     if (diff.hasDiff()) {
    937                         if (diff.added) {
    938                             synchronized (sLock) {
    939                                 for (ProjectState projectState : sProjectStateMap.values()) {
    940                                     if (projectState != state) {
    941                                         // need to call needs to do the libraryState link,
    942                                         // but no need to look at the result, as we'll compare
    943                                         // the result of getFullLibraryProjects()
    944                                         // this is easier to due to indirect dependencies.
    945                                         state.needs(projectState);
    946                                     }
    947                                 }
    948                             }
    949                         }
    950 
    951                         // and build the real difference. A list of new projects and a list of
    952                         // removed project.
    953                         // This is not the same as the added/removed libraries because libraries
    954                         // could be indirect dependencies through several different direct
    955                         // dependencies so it's easier to compare the full lists before and after
    956                         // the reload.
    957                         LinkUpdateBundle bundle = getLinkBundle(state, oldLibraries);
    958                         if (bundle != null) {
    959                             queueLinkUpdateBundle(bundle);
    960                         }
    961                     }
    962 
    963                     // apply the new target if needed.
    964                     if (newTarget != oldTarget) {
    965                         IJavaProject javaProject = BaseProjectHelper.getJavaProject(
    966                                 file.getProject());
    967                         if (javaProject != null) {
    968                             AndroidClasspathContainerInitializer.updateProjects(
    969                                     new IJavaProject[] { javaProject });
    970                         }
    971 
    972                         // update the editors to reload with the new target
    973                         AdtPlugin.getDefault().updateTargetListeners(iProject);
    974                     }
    975                 } catch (CoreException e) {
    976                     // This can't happen as it's only for closed project (or non existing)
    977                     // but in that case we can't get a fileChanged on this file.
    978                 }
    979             }
    980         }
    981     };
    982 
    983     /** List of opened project. This is filled in {@link IProjectListener#projectOpened(IProject)}
    984      * and {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, and processed in
    985      * {@link IResourceEventListener#resourceChangeEventEnd()}.
    986      */
    987     private final ArrayList<ProjectState> mOpenedLibraryProjects = new ArrayList<ProjectState>();
    988 
    989     /**
    990      * Delegate listener for resource changes. This is called before and after any calls to the
    991      * project and file listeners (for a given resource change event).
    992      */
    993     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
    994         public void resourceChangeEventStart() {
    995             // pass
    996         }
    997 
    998         public void resourceChangeEventEnd() {
    999             updateProjectsWithNewLibraries(mOpenedLibraryProjects);
   1000             mOpenedLibraryProjects.clear();
   1001         }
   1002     };
   1003 
   1004     /**
   1005      * Action bundle to update library links on a project.
   1006      *
   1007      * @see Sdk#queueLinkUpdateBundle(LinkUpdateBundle)
   1008      * @see Sdk#updateLibraryLinks(LinkUpdateBundle, IProgressMonitor)
   1009      */
   1010     private static class LinkUpdateBundle {
   1011 
   1012         /** The main project receiving the library links. */
   1013         IProject mProject = null;
   1014         /** A list (possibly null/empty) of projects that should be linked. */
   1015         IProject[] mNewLibraryProjects = null;
   1016         /** an optional old library path that needs to be removed at the same time as the new
   1017          * libraries are added. Can be <code>null</code> in which case no libraries are removed. */
   1018         IPath mDeletedLibraryPath = null;
   1019         /** A list (possibly null/empty) of projects that should be unlinked */
   1020         IProject[] mRemovedLibraryProjects = null;
   1021         /** Whether unknown IClasspathEntry (that were flagged as being added by ADT) are to be
   1022          * removed. This is typically only set to <code>true</code> when the project is opened. */
   1023         boolean mCleanupCPE = false;
   1024 
   1025         @Override
   1026         public String toString() {
   1027             return String.format(
   1028                     "LinkUpdateBundle: %1$s (clean: %2$s) > added: %3$s, removed: %4$s, deleted: %5$s", //$NON-NLS-1$
   1029                     mProject.getName(),
   1030                     mCleanupCPE,
   1031                     Arrays.toString(mNewLibraryProjects),
   1032                     Arrays.toString(mRemovedLibraryProjects),
   1033                     mDeletedLibraryPath);
   1034         }
   1035     }
   1036 
   1037     private final ArrayList<LinkUpdateBundle> mLinkActionBundleQueue =
   1038             new ArrayList<LinkUpdateBundle>();
   1039 
   1040     /**
   1041      * Queues a {@link LinkUpdateBundle} bundle to be run by a job.
   1042      *
   1043      * All action bundles are executed in a job in the exact order they are added.
   1044      * This is convenient when several actions must be executed in a job consecutively (instead
   1045      * of in parallel as it would happen if each started its own job) but it is impossible
   1046      * to manually control the job that's running them (for instance each action is started from
   1047      * different callbacks such as {@link IProjectListener#projectOpened(IProject)}.
   1048      *
   1049      * If the job is not yet started, or has terminated due to lack of action bundle, it is
   1050      * restarted.
   1051      *
   1052      * @param bundle the action bundle to execute
   1053      */
   1054     private void queueLinkUpdateBundle(LinkUpdateBundle bundle) {
   1055         boolean startJob = false;
   1056         synchronized (mLinkActionBundleQueue) {
   1057             startJob = mLinkActionBundleQueue.size() == 0;
   1058             mLinkActionBundleQueue.add(bundle);
   1059         }
   1060 
   1061         if (startJob) {
   1062             Job job = new Job("Android Library Update") { //$NON-NLS-1$
   1063                 @Override
   1064                 protected IStatus run(IProgressMonitor monitor) {
   1065                     // loop until there's no bundle to process
   1066                     while (true) {
   1067                         // get the bundle, but don't remove until we're done, or a new job could be
   1068                         // started.
   1069                         LinkUpdateBundle bundle = null;
   1070                         synchronized (mLinkActionBundleQueue) {
   1071                             // there is always a bundle at this point, as they are only removed
   1072                             // at the end of this method, and the job is only started after adding
   1073                             // one
   1074                             bundle = mLinkActionBundleQueue.get(0);
   1075                         }
   1076 
   1077                         // process the bundle.
   1078                         try {
   1079                             updateLibraryLinks(bundle, monitor);
   1080                         } catch (Exception e) {
   1081                             AdtPlugin.log(e, "Failed to process bundle: %1$s", //$NON-NLS-1$
   1082                                     bundle.toString());
   1083                         }
   1084 
   1085                         try {
   1086                             // force a recompile
   1087                             bundle.mProject.build(IncrementalProjectBuilder.FULL_BUILD, monitor);
   1088                         } catch (Exception e) {
   1089                             // no need to log those.
   1090                         }
   1091 
   1092                         // remove it from the list.
   1093                         synchronized (mLinkActionBundleQueue) {
   1094                             mLinkActionBundleQueue.remove(0);
   1095 
   1096                             // no more bundle to process? done.
   1097                             if (mLinkActionBundleQueue.size() == 0) {
   1098                                 return Status.OK_STATUS;
   1099                             }
   1100                         }
   1101                     }
   1102                 }
   1103             };
   1104             job.setPriority(Job.BUILD);
   1105             job.schedule();
   1106         }
   1107     }
   1108 
   1109 
   1110     /**
   1111      * Adds to a list the resolved {@link IProject} dependencies for a given {@link ProjectState}.
   1112      * This recursively goes down to indirect dependencies.
   1113      *
   1114      * <strong>The list is filled in an order that is not valid for calling <code>aapt</code>
   1115      * </strong>.
   1116      * Use {@link ProjectState#getFullLibraryProjects()} for use with <code>aapt</code>.
   1117      *
   1118      * @param projectState the ProjectState of the project from which to add the libraries.
   1119      * @param libraries the list of {@link IProject} to fill.
   1120      */
   1121     private void fillProjectDependenciesList(ProjectState projectState,
   1122             ArrayList<IProject> libraries) {
   1123         for (LibraryState libState : projectState.getLibraries()) {
   1124             ProjectState libProjectState = libState.getProjectState();
   1125 
   1126             // only care if the LibraryState has a resolved ProjectState
   1127             if (libProjectState != null) {
   1128                 // try not to add duplicate. This can happen if a project depends on 2 different
   1129                 // libraries that both depend on the same one.
   1130                 IProject libProject = libProjectState.getProject();
   1131                 if (libraries.contains(libProject) == false) {
   1132                     libraries.add(libProject);
   1133                 }
   1134 
   1135                 // process the libraries of this library too.
   1136                 fillProjectDependenciesList(libProjectState, libraries);
   1137             }
   1138         }
   1139     }
   1140 
   1141     /**
   1142      * Sets up a path variable for a given project.
   1143      * The name of the variable is based on the name of the project. However some valid character
   1144      * for project names can be invalid for variable paths.
   1145      * {@link #getLibraryVariableName(String)} return the name of the variable based on the
   1146      * project name.
   1147      *
   1148      * @param libProject the project
   1149      *
   1150      * @see IPathVariableManager
   1151      * @see #getLibraryVariableName(String)
   1152      */
   1153     private void setupLibraryProject(IProject libProject) {
   1154         // if needed add a path var for this library
   1155         IPathVariableManager pathVarMgr =
   1156             ResourcesPlugin.getWorkspace().getPathVariableManager();
   1157         IPath libPath = libProject.getLocation();
   1158 
   1159         final String varName = getLibraryVariableName(libProject.getName());
   1160 
   1161         if (libPath.equals(pathVarMgr.getValue(varName)) == false) {
   1162             try {
   1163                 pathVarMgr.setValue(varName, libPath);
   1164             } catch (CoreException e) {
   1165                 AdtPlugin.logAndPrintError(e, "Library Project",
   1166                         "Unable to set linked path var '%1$s' for library %2$s: %3$s", //$NON-NLS-1$
   1167                         varName, libPath.toOSString(), e.getMessage());
   1168             }
   1169         }
   1170     }
   1171 
   1172 
   1173     /**
   1174      * Deletes the path variable that was setup for the given project.
   1175      * @param project the project
   1176      * @see #disposeLibraryProject(String)
   1177      */
   1178     private void disposeLibraryProject(IProject project) {
   1179         disposeLibraryProject(project.getName());
   1180     }
   1181 
   1182     /**
   1183      * Deletes the path variable that was setup for the given project name.
   1184      * The name of the variable is based on the name of the project. However some valid character
   1185      * for project names can be invalid for variable paths.
   1186      * {@link #getLibraryVariableName(String)} return the name of the variable based on the
   1187      * project name.
   1188      * @param projectName the name of the project, unmodified.
   1189      */
   1190     private void disposeLibraryProject(String projectName) {
   1191         IPathVariableManager pathVarMgr =
   1192             ResourcesPlugin.getWorkspace().getPathVariableManager();
   1193 
   1194         final String varName = getLibraryVariableName(projectName);
   1195 
   1196         // remove the value by setting the value to null.
   1197         try {
   1198             pathVarMgr.setValue(varName, null /*path*/);
   1199         } catch (CoreException e) {
   1200             String message = String.format("Unable to remove linked path var '%1$s'", //$NON-NLS-1$
   1201                     varName);
   1202             AdtPlugin.log(e, message);
   1203         }
   1204     }
   1205 
   1206     /**
   1207      * Returns a valid path variable name based on the name of a library project.
   1208      * @param name the name of the library project.
   1209      */
   1210     private String getLibraryVariableName(String name) {
   1211         return "_android_" + name.replaceAll("-", "_"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
   1212     }
   1213 
   1214     /**
   1215      * Update the library links for a project
   1216      *
   1217      * This does the follow:
   1218      * - add/remove the library projects to the main projects dynamic reference list. This is used
   1219      *   by the builders to receive resource change deltas for library projects and figure out what
   1220      *   needs to be recompiled/recreated.
   1221      * - create new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each
   1222      *   source folder for each new library project.
   1223      * - remove the {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each
   1224      *   source folder for each removed library project.
   1225      * - If {@link LinkUpdateBundle#mCleanupCPE} is set to true, all CPE created by ADT that cannot
   1226      *   be resolved are removed. This should only be used when the project is opened.
   1227      *
   1228      * <strong>This must not be called directly. Instead the {@link LinkUpdateBundle} must
   1229      * be run through a job with {@link #queueLinkUpdateBundle(LinkUpdateBundle)}.</strong>
   1230      *
   1231      * @param bundle The {@link LinkUpdateBundle} action bundle that contains all the parameters
   1232      *               necessary to execute the action.
   1233      * @param monitor an {@link IProgressMonitor}.
   1234      * @return an {@link IStatus} with the status of the action.
   1235      */
   1236     private IStatus updateLibraryLinks(LinkUpdateBundle bundle, IProgressMonitor monitor) {
   1237         if (bundle.mProject.isOpen() == false) {
   1238             return Status.OK_STATUS;
   1239         }
   1240         try {
   1241             // add the library to the list of dynamic references. This is necessary to receive
   1242             // notifications that the library content changed in the builders.
   1243             IProjectDescription projectDescription = bundle.mProject.getDescription();
   1244             IProject[] refs = projectDescription.getDynamicReferences();
   1245 
   1246             if (refs.length > 0) {
   1247                 ArrayList<IProject> list = new ArrayList<IProject>(Arrays.asList(refs));
   1248 
   1249                 // remove a previous library if needed (in case of a rename)
   1250                 if (bundle.mDeletedLibraryPath != null) {
   1251                     // since project basically have only one segment that matter,
   1252                     // just check the names
   1253                     removeFromList(list, bundle.mDeletedLibraryPath.lastSegment());
   1254                 }
   1255 
   1256                 if (bundle.mRemovedLibraryProjects != null) {
   1257                     for (IProject removedProject : bundle.mRemovedLibraryProjects) {
   1258                         removeFromList(list, removedProject.getName());
   1259                     }
   1260                 }
   1261 
   1262                 // add the new ones if they don't exist
   1263                 if (bundle.mNewLibraryProjects != null) {
   1264                     for (IProject newProject : bundle.mNewLibraryProjects) {
   1265                         if (list.contains(newProject) == false) {
   1266                             list.add(newProject);
   1267                         }
   1268                     }
   1269                 }
   1270 
   1271                 // set the changed list
   1272                 projectDescription.setDynamicReferences(
   1273                         list.toArray(new IProject[list.size()]));
   1274             } else {
   1275                 if (bundle.mNewLibraryProjects != null) {
   1276                     projectDescription.setDynamicReferences(bundle.mNewLibraryProjects);
   1277                 }
   1278             }
   1279 
   1280             // get the current classpath entries for the project to add the new source
   1281             // folders.
   1282             IJavaProject javaProject = JavaCore.create(bundle.mProject);
   1283             IClasspathEntry[] entries = javaProject.getRawClasspath();
   1284             ArrayList<IClasspathEntry> classpathEntries = new ArrayList<IClasspathEntry>(
   1285                     Arrays.asList(entries));
   1286 
   1287             IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
   1288 
   1289             // loop on the classpath entries and look for CPE_SOURCE entries that
   1290             // are linked folders, then record them for comparison later as we add the new
   1291             // ones.
   1292             ArrayList<IClasspathEntry> cpeToRemove = new ArrayList<IClasspathEntry>();
   1293             for (IClasspathEntry classpathEntry : classpathEntries) {
   1294                 if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
   1295                     IPath path = classpathEntry.getPath();
   1296                     IResource linkedRes = wsRoot.findMember(path);
   1297                     if (linkedRes != null && linkedRes.isLinked() &&
   1298                             CREATOR_ADT.equals(ProjectHelper.loadStringProperty(
   1299                                     linkedRes, PROP_CREATOR))) {
   1300 
   1301                         // add always to list if we're doing clean-up
   1302                         if (bundle.mCleanupCPE) {
   1303                             cpeToRemove.add(classpathEntry);
   1304                         } else {
   1305                             String libName = ProjectHelper.loadStringProperty(linkedRes,
   1306                                     PROP_LIBRARY_NAME);
   1307                             if (libName != null && isRemovedLibrary(bundle, libName)) {
   1308                                 cpeToRemove.add(classpathEntry);
   1309                             }
   1310                         }
   1311                     }
   1312                 }
   1313             }
   1314 
   1315             // loop on the projects to add.
   1316             if (bundle.mNewLibraryProjects != null) {
   1317                 for (IProject library : bundle.mNewLibraryProjects) {
   1318                     if (library.isOpen() == false) {
   1319                         continue;
   1320                     }
   1321                     final String libName = library.getName();
   1322                     final String varName = getLibraryVariableName(libName);
   1323 
   1324                     // get the list of source folders for the library.
   1325                     ArrayList<IPath> sourceFolderPaths = BaseProjectHelper.getSourceClasspaths(
   1326                             library);
   1327 
   1328                     // loop on all the source folder, ignoring FD_GEN and add them
   1329                     // as linked folder
   1330                     for (IPath sourceFolderPath : sourceFolderPaths) {
   1331                         IResource sourceFolder = wsRoot.findMember(sourceFolderPath);
   1332                         if (sourceFolder == null || sourceFolder.isLinked()) {
   1333                             continue;
   1334                         }
   1335 
   1336                         IPath relativePath = sourceFolder.getProjectRelativePath();
   1337                         if (SdkConstants.FD_GEN_SOURCES.equals(relativePath.toString())) {
   1338                             continue;
   1339                         }
   1340 
   1341                         // create the linked path
   1342                         IPath linkedPath = new Path(varName).append(relativePath);
   1343 
   1344                         // look for an existing CPE that has the same linked path and that was
   1345                         // going to be removed.
   1346                         IClasspathEntry match = findClasspathEntryMatch(cpeToRemove, linkedPath,
   1347                                 null);
   1348 
   1349                         if (match == null) {
   1350                             // no match, create one
   1351                             // get a string version, to make up the linked folder name
   1352                             String srcFolderName = relativePath.toString().replace(
   1353                                     "/",  //$NON-NLS-1$
   1354                                     "_"); //$NON-NLS-1$
   1355 
   1356                             // folder name
   1357                             String folderName = libName + "_" + srcFolderName; //$NON-NLS-1$
   1358 
   1359                             // create a linked resource for the library using the path var.
   1360                             IFolder libSrc = bundle.mProject.getFolder(folderName);
   1361                             IPath libSrcPath = libSrc.getFullPath();
   1362 
   1363                             // check if there's a CPE that would conflict, in which case it needs to
   1364                             // be removed (this can happen for existing CPE that don't match an open
   1365                             // project)
   1366                             match = findClasspathEntryMatch(classpathEntries, null/*rawPath*/,
   1367                                     libSrcPath);
   1368                             if (match != null) {
   1369                                 classpathEntries.remove(match);
   1370                             }
   1371 
   1372                             // the path of the linked resource is based on the path variable
   1373                             // representing the library project, followed by the source folder name.
   1374                             libSrc.createLink(linkedPath, IResource.REPLACE, monitor);
   1375 
   1376                             // set some persistent properties on it to know that it was
   1377                             // created by ADT.
   1378                             ProjectHelper.saveStringProperty(libSrc, PROP_CREATOR, CREATOR_ADT);
   1379                             ProjectHelper.saveResourceProperty(libSrc, PROP_LIBRARY, library);
   1380                             ProjectHelper.saveStringProperty(libSrc, PROP_LIBRARY_NAME,
   1381                                     library.getName());
   1382 
   1383                             // add the source folder to the classpath entries
   1384                             classpathEntries.add(JavaCore.newSourceEntry(libSrcPath));
   1385                         } else {
   1386                             // there's a valid match, do nothing, but remove the match from
   1387                             // the list of previously existing CPE.
   1388                             cpeToRemove.remove(match);
   1389                         }
   1390                     }
   1391                 }
   1392             }
   1393 
   1394             // remove the CPE that should be removed.
   1395             classpathEntries.removeAll(cpeToRemove);
   1396 
   1397             // set the new list
   1398             javaProject.setRawClasspath(
   1399                     classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]),
   1400                     monitor);
   1401 
   1402             // and delete the folders of the CPE that were removed (must be done after)
   1403             for (IClasspathEntry cpe : cpeToRemove) {
   1404                 IResource res = wsRoot.findMember(cpe.getPath());
   1405                 res.delete(true, monitor);
   1406             }
   1407 
   1408             return Status.OK_STATUS;
   1409         } catch (CoreException e) {
   1410             AdtPlugin.logAndPrintError(e, bundle.mProject.getName(),
   1411                     "Failed to create library links: %1$s", //$NON-NLS-1$
   1412                     e.getMessage());
   1413             return e.getStatus();
   1414         }
   1415     }
   1416 
   1417     private boolean isRemovedLibrary(LinkUpdateBundle bundle, String libName) {
   1418         if (bundle.mDeletedLibraryPath != null &&
   1419                 libName.equals(bundle.mDeletedLibraryPath.lastSegment())) {
   1420             return true;
   1421         }
   1422 
   1423         if (bundle.mRemovedLibraryProjects != null) {
   1424             for (IProject removedProject : bundle.mRemovedLibraryProjects) {
   1425                 if (libName.equals(removedProject.getName())) {
   1426                     return true;
   1427                 }
   1428             }
   1429         }
   1430 
   1431         return false;
   1432     }
   1433 
   1434     /**
   1435      * Computes the library difference based on a previous list and a current state, and creates
   1436      * a {@link LinkUpdateBundle} action to update the given project.
   1437      * @param project The current project state
   1438      * @param oldLibraries the list of old libraries. Typically the result of
   1439      *            {@link ProjectState#getFullLibraryProjects()} before the ProjectState is updated.
   1440      * @return null if there no action to take, or a {@link LinkUpdateBundle} object to run.
   1441      */
   1442     private LinkUpdateBundle getLinkBundle(ProjectState project, IProject[] oldLibraries) {
   1443         // get the new full list of projects
   1444         IProject[] newLibraries = project.getFullLibraryProjects();
   1445 
   1446         // and build the real difference. A list of new projects and a list of
   1447         // removed project.
   1448         // This is not the same as the added/removed libraries because libraries
   1449         // could be indirect dependencies through several different direct
   1450         // dependencies so it's easier to compare the full lists before and after
   1451         // the reload.
   1452 
   1453         List<IProject> addedLibs = new ArrayList<IProject>();
   1454         List<IProject> removedLibs = new ArrayList<IProject>();
   1455 
   1456         // first get the list of new projects.
   1457         for (IProject newLibrary : newLibraries) {
   1458             boolean found = false;
   1459             for (IProject oldLibrary : oldLibraries) {
   1460                 if (newLibrary.equals(oldLibrary)) {
   1461                     found = true;
   1462                     break;
   1463                 }
   1464             }
   1465 
   1466             // if it was not found in the old libraries, it's really new
   1467             if (found == false) {
   1468                 addedLibs.add(newLibrary);
   1469             }
   1470         }
   1471 
   1472         // now the list of removed projects.
   1473         for (IProject oldLibrary : oldLibraries) {
   1474             boolean found = false;
   1475             for (IProject newLibrary : newLibraries) {
   1476                 if (newLibrary.equals(oldLibrary)) {
   1477                     found = true;
   1478                     break;
   1479                 }
   1480             }
   1481 
   1482             // if it was not found in the new libraries, it's really been removed
   1483             if (found == false) {
   1484                 removedLibs.add(oldLibrary);
   1485             }
   1486         }
   1487 
   1488         if (addedLibs.size() > 0 || removedLibs.size() > 0) {
   1489             LinkUpdateBundle bundle = new LinkUpdateBundle();
   1490             bundle.mProject = project.getProject();
   1491             bundle.mNewLibraryProjects =
   1492                 addedLibs.toArray(new IProject[addedLibs.size()]);
   1493             bundle.mRemovedLibraryProjects =
   1494                 removedLibs.toArray(new IProject[removedLibs.size()]);
   1495             return bundle;
   1496         }
   1497 
   1498         return null;
   1499     }
   1500 
   1501     /**
   1502      * Removes a project from a list based on its name.
   1503      * @param projects the list of projects.
   1504      * @param name the name of the project to remove.
   1505      */
   1506     private void removeFromList(List<IProject> projects, String name) {
   1507         final int count = projects.size();
   1508         for (int i = 0 ; i < count ; i++) {
   1509             // since project basically have only one segment that matter,
   1510             // just check the names
   1511             if (projects.get(i).getName().equals(name)) {
   1512                 projects.remove(i);
   1513                 return;
   1514             }
   1515         }
   1516     }
   1517 
   1518     /**
   1519      * Returns a {@link IClasspathEntry} from the given list whose linked path match the given path.
   1520      * @param cpeList a list of {@link IClasspathEntry} of {@link IClasspathEntry#getEntryKind()}
   1521      *                {@link IClasspathEntry#CPE_SOURCE} whose {@link IClasspathEntry#getPath()}
   1522      *                points to a linked folder.
   1523      * @param rawPath the raw path to compare to. Can be null if <var>path</var> is used instead.
   1524      * @param path the path to compare to. Can be null if <var>rawPath</var> is used instead.
   1525      * @return the matching IClasspathEntry or null.
   1526      */
   1527     private IClasspathEntry findClasspathEntryMatch(ArrayList<IClasspathEntry> cpeList,
   1528             IPath rawPath, IPath path) {
   1529         IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
   1530         for (IClasspathEntry cpe : cpeList) {
   1531             IPath cpePath = cpe.getPath();
   1532             // test the normal path of the resource.
   1533             if (path != null && path.equals(cpePath)) {
   1534                 return cpe;
   1535             }
   1536 
   1537             IResource res = wsRoot.findMember(cpePath);
   1538             // getRawLocation returns the path that the linked folder points to.
   1539             if (rawPath != null && res.getRawLocation().equals(rawPath)) {
   1540                 return cpe;
   1541             }
   1542 
   1543         }
   1544         return null;
   1545     }
   1546 
   1547     /**
   1548      * Updates all existing projects with a given list of new/updated libraries.
   1549      * This loops through all opened projects and check if they depend on any of the given
   1550      * library project, and if they do, they are linked together.
   1551      * @param libraries the list of new/updated library projects.
   1552      */
   1553     private void updateProjectsWithNewLibraries(List<ProjectState> libraries) {
   1554         if (libraries.size() == 0) {
   1555             return;
   1556         }
   1557 
   1558         ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
   1559         synchronized (sLock) {
   1560             // for each projects, look for projects that depend on it, and update them.
   1561             // Once they are updated (meaning ProjectState#needs() has been called on them),
   1562             // we add them to the list so that can be updated as well.
   1563             for (ProjectState projectState : sProjectStateMap.values()) {
   1564                 // record the current library dependencies
   1565                 IProject[] oldLibraries = projectState.getFullLibraryProjects();
   1566 
   1567                 boolean needLibraryDependenciesUpdated = false;
   1568                 for (ProjectState library : libraries) {
   1569                     // Normally we would only need to test if ProjectState#needs returns non null,
   1570                     // meaning the link between the project and the library has not been
   1571                     // done yet.
   1572                     // However what matters here is that the library is a dependency,
   1573                     // period. If the library project was updated, then we redo the link,
   1574                     // with all indirect dependencies (which *have* changed, since this is
   1575                     // what this method is all about.)
   1576                     // We still need to call ProjectState#needs to make the link in case it's not
   1577                     // been done yet (which can happen if the library project was just opened).
   1578                     if (projectState != library) {
   1579                         // call needs in case this new library was just opened, and the link needs
   1580                         // to be done
   1581                         LibraryState libState = projectState.needs(library);
   1582                         if (libState == null && projectState.dependsOn(library)) {
   1583                             // ProjectState.needs only returns true if the library was needed.
   1584                             // but we also need to check the case where the project depends on
   1585                             // the library but the link was already done.
   1586                             needLibraryDependenciesUpdated = true;
   1587                         }
   1588                     }
   1589                 }
   1590 
   1591                 if (needLibraryDependenciesUpdated) {
   1592                     projectState.updateFullLibraryList();
   1593                 }
   1594 
   1595                 LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
   1596                 if (bundle != null) {
   1597                     queueLinkUpdateBundle(bundle);
   1598 
   1599                     // if this updated project is a library, add it to the list, so that
   1600                     // projects depending on it get updated too.
   1601                     if (projectState.isLibrary() &&
   1602                             updatedLibraries.contains(projectState) == false) {
   1603                         updatedLibraries.add(projectState);
   1604                     }
   1605                 }
   1606             }
   1607         }
   1608 
   1609         // done, but there may be updated projects that were libraries, so we need to do the same
   1610         // for this libraries, to update the project there were depending on.
   1611         updateProjectsWithNewLibraries(updatedLibraries);
   1612     }
   1613 
   1614     /**
   1615      * Computes a new IPath targeting a given target, but relative to a given base.
   1616      * <p/>{@link IPath#makeRelativeTo(IPath, IPath)} is only available in 3.5 and later.
   1617      * <p/>This is based on the implementation {@link Path#makeRelativeTo(IPath)}.
   1618      * @param target the target of the IPath
   1619      * @param base the IPath to base the relative path on.
   1620      * @return the relative IPath
   1621      */
   1622     public static IPath makeRelativeTo(IPath target, IPath base) {
   1623         //can't make relative if devices are not equal
   1624         if (target.getDevice() != base.getDevice() && (target.getDevice() == null ||
   1625                 !target.getDevice().equalsIgnoreCase(base.getDevice())))
   1626             return target;
   1627         int commonLength = target.matchingFirstSegments(base);
   1628         final int differenceLength = base.segmentCount() - commonLength;
   1629         final int newSegmentLength = differenceLength + target.segmentCount() - commonLength;
   1630         if (newSegmentLength == 0)
   1631             return Path.EMPTY;
   1632         String[] newSegments = new String[newSegmentLength];
   1633         //add parent references for each segment different from the base
   1634         Arrays.fill(newSegments, 0, differenceLength, ".."); //$NON-NLS-1$
   1635         //append the segments of this path not in common with the base
   1636         System.arraycopy(target.segments(), commonLength, newSegments,
   1637                 differenceLength, newSegmentLength - differenceLength);
   1638 
   1639         StringBuilder sb = new StringBuilder();
   1640         for (String s : newSegments) {
   1641             sb.append(s).append('/');
   1642         }
   1643 
   1644         return new Path(null, sb.toString());
   1645     }
   1646 }
   1647 
   1648