OSDN Git Service

Improve library link/unlink again.
[android-x86/sdk.git] / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / sdk / Sdk.java
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                             // mark it as derived so that Team plug-in ignore this
1377                             libSrc.setDerived(true);
1378
1379                             // set some persistent properties on it to know that it was
1380                             // created by ADT.
1381                             ProjectHelper.saveStringProperty(libSrc, PROP_CREATOR, CREATOR_ADT);
1382                             ProjectHelper.saveResourceProperty(libSrc, PROP_LIBRARY, library);
1383                             ProjectHelper.saveStringProperty(libSrc, PROP_LIBRARY_NAME,
1384                                     library.getName());
1385
1386                             // add the source folder to the classpath entries
1387                             classpathEntries.add(JavaCore.newSourceEntry(libSrcPath));
1388                         } else {
1389                             // there's a valid match, do nothing, but remove the match from
1390                             // the list of previously existing CPE.
1391                             cpeToRemove.remove(match);
1392                         }
1393                     }
1394                 }
1395             }
1396
1397             // remove the CPE that should be removed.
1398             classpathEntries.removeAll(cpeToRemove);
1399
1400             // set the new list
1401             javaProject.setRawClasspath(
1402                     classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]),
1403                     monitor);
1404
1405             // and delete the folders of the CPE that were removed (must be done after)
1406             for (IClasspathEntry cpe : cpeToRemove) {
1407                 IResource res = wsRoot.findMember(cpe.getPath());
1408                 res.delete(true, monitor);
1409             }
1410
1411             return Status.OK_STATUS;
1412         } catch (CoreException e) {
1413             AdtPlugin.logAndPrintError(e, bundle.mProject.getName(),
1414                     "Failed to create library links: %1$s", //$NON-NLS-1$
1415                     e.getMessage());
1416             return e.getStatus();
1417         }
1418     }
1419
1420     private boolean isRemovedLibrary(LinkUpdateBundle bundle, String libName) {
1421         if (bundle.mDeletedLibraryPath != null &&
1422                 libName.equals(bundle.mDeletedLibraryPath.lastSegment())) {
1423             return true;
1424         }
1425
1426         if (bundle.mRemovedLibraryProjects != null) {
1427             for (IProject removedProject : bundle.mRemovedLibraryProjects) {
1428                 if (libName.equals(removedProject.getName())) {
1429                     return true;
1430                 }
1431             }
1432         }
1433
1434         return false;
1435     }
1436
1437     /**
1438      * Computes the library difference based on a previous list and a current state, and creates
1439      * a {@link LinkUpdateBundle} action to update the given project.
1440      * @param project The current project state
1441      * @param oldLibraries the list of old libraries. Typically the result of
1442      *            {@link ProjectState#getFullLibraryProjects()} before the ProjectState is updated.
1443      * @return null if there no action to take, or a {@link LinkUpdateBundle} object to run.
1444      */
1445     private LinkUpdateBundle getLinkBundle(ProjectState project, IProject[] oldLibraries) {
1446         // get the new full list of projects
1447         IProject[] newLibraries = project.getFullLibraryProjects();
1448
1449         // and build the real difference. A list of new projects and a list of
1450         // removed project.
1451         // This is not the same as the added/removed libraries because libraries
1452         // could be indirect dependencies through several different direct
1453         // dependencies so it's easier to compare the full lists before and after
1454         // the reload.
1455
1456         List<IProject> addedLibs = new ArrayList<IProject>();
1457         List<IProject> removedLibs = new ArrayList<IProject>();
1458
1459         // first get the list of new projects.
1460         for (IProject newLibrary : newLibraries) {
1461             boolean found = false;
1462             for (IProject oldLibrary : oldLibraries) {
1463                 if (newLibrary.equals(oldLibrary)) {
1464                     found = true;
1465                     break;
1466                 }
1467             }
1468
1469             // if it was not found in the old libraries, it's really new
1470             if (found == false) {
1471                 addedLibs.add(newLibrary);
1472             }
1473         }
1474
1475         // now the list of removed projects.
1476         for (IProject oldLibrary : oldLibraries) {
1477             boolean found = false;
1478             for (IProject newLibrary : newLibraries) {
1479                 if (newLibrary.equals(oldLibrary)) {
1480                     found = true;
1481                     break;
1482                 }
1483             }
1484
1485             // if it was not found in the new libraries, it's really been removed
1486             if (found == false) {
1487                 removedLibs.add(oldLibrary);
1488             }
1489         }
1490
1491         if (addedLibs.size() > 0 || removedLibs.size() > 0) {
1492             LinkUpdateBundle bundle = new LinkUpdateBundle();
1493             bundle.mProject = project.getProject();
1494             bundle.mNewLibraryProjects =
1495                 addedLibs.toArray(new IProject[addedLibs.size()]);
1496             bundle.mRemovedLibraryProjects =
1497                 removedLibs.toArray(new IProject[removedLibs.size()]);
1498             return bundle;
1499         }
1500
1501         return null;
1502     }
1503
1504     /**
1505      * Removes a project from a list based on its name.
1506      * @param projects the list of projects.
1507      * @param name the name of the project to remove.
1508      */
1509     private void removeFromList(List<IProject> projects, String name) {
1510         final int count = projects.size();
1511         for (int i = 0 ; i < count ; i++) {
1512             // since project basically have only one segment that matter,
1513             // just check the names
1514             if (projects.get(i).getName().equals(name)) {
1515                 projects.remove(i);
1516                 return;
1517             }
1518         }
1519     }
1520
1521     /**
1522      * Returns a {@link IClasspathEntry} from the given list whose linked path match the given path.
1523      * @param cpeList a list of {@link IClasspathEntry} of {@link IClasspathEntry#getEntryKind()}
1524      *                {@link IClasspathEntry#CPE_SOURCE} whose {@link IClasspathEntry#getPath()}
1525      *                points to a linked folder.
1526      * @param rawPath the raw path to compare to. Can be null if <var>path</var> is used instead.
1527      * @param path the path to compare to. Can be null if <var>rawPath</var> is used instead.
1528      * @return the matching IClasspathEntry or null.
1529      */
1530     private IClasspathEntry findClasspathEntryMatch(ArrayList<IClasspathEntry> cpeList,
1531             IPath rawPath, IPath path) {
1532         IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
1533         for (IClasspathEntry cpe : cpeList) {
1534             IPath cpePath = cpe.getPath();
1535             // test the normal path of the resource.
1536             if (path != null && path.equals(cpePath)) {
1537                 return cpe;
1538             }
1539
1540             IResource res = wsRoot.findMember(cpePath);
1541             // getRawLocation returns the path that the linked folder points to.
1542             if (rawPath != null && res.getRawLocation().equals(rawPath)) {
1543                 return cpe;
1544             }
1545
1546         }
1547         return null;
1548     }
1549
1550     /**
1551      * Updates all existing projects with a given list of new/updated libraries.
1552      * This loops through all opened projects and check if they depend on any of the given
1553      * library project, and if they do, they are linked together.
1554      * @param libraries the list of new/updated library projects.
1555      */
1556     private void updateProjectsWithNewLibraries(List<ProjectState> libraries) {
1557         if (libraries.size() == 0) {
1558             return;
1559         }
1560
1561         ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
1562         synchronized (sLock) {
1563             // for each projects, look for projects that depend on it, and update them.
1564             // Once they are updated (meaning ProjectState#needs() has been called on them),
1565             // we add them to the list so that can be updated as well.
1566             for (ProjectState projectState : sProjectStateMap.values()) {
1567                 // record the current library dependencies
1568                 IProject[] oldLibraries = projectState.getFullLibraryProjects();
1569
1570                 boolean needLibraryDependenciesUpdated = false;
1571                 for (ProjectState library : libraries) {
1572                     // Normally we would only need to test if ProjectState#needs returns non null,
1573                     // meaning the link between the project and the library has not been
1574                     // done yet.
1575                     // However what matters here is that the library is a dependency,
1576                     // period. If the library project was updated, then we redo the link,
1577                     // with all indirect dependencies (which *have* changed, since this is
1578                     // what this method is all about.)
1579                     // We still need to call ProjectState#needs to make the link in case it's not
1580                     // been done yet (which can happen if the library project was just opened).
1581                     if (projectState != library) {
1582                         // call needs in case this new library was just opened, and the link needs
1583                         // to be done
1584                         LibraryState libState = projectState.needs(library);
1585                         if (libState == null && projectState.dependsOn(library)) {
1586                             // ProjectState.needs only returns true if the library was needed.
1587                             // but we also need to check the case where the project depends on
1588                             // the library but the link was already done.
1589                             needLibraryDependenciesUpdated = true;
1590                         }
1591                     }
1592                 }
1593
1594                 if (needLibraryDependenciesUpdated) {
1595                     projectState.updateFullLibraryList();
1596                 }
1597
1598                 LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
1599                 if (bundle != null) {
1600                     queueLinkUpdateBundle(bundle);
1601
1602                     // if this updated project is a library, add it to the list, so that
1603                     // projects depending on it get updated too.
1604                     if (projectState.isLibrary() &&
1605                             updatedLibraries.contains(projectState) == false) {
1606                         updatedLibraries.add(projectState);
1607                     }
1608                 }
1609             }
1610         }
1611
1612         // done, but there may be updated projects that were libraries, so we need to do the same
1613         // for this libraries, to update the project there were depending on.
1614         updateProjectsWithNewLibraries(updatedLibraries);
1615     }
1616
1617     /**
1618      * Computes a new IPath targeting a given target, but relative to a given base.
1619      * <p/>{@link IPath#makeRelativeTo(IPath, IPath)} is only available in 3.5 and later.
1620      * <p/>This is based on the implementation {@link Path#makeRelativeTo(IPath)}.
1621      * @param target the target of the IPath
1622      * @param base the IPath to base the relative path on.
1623      * @return the relative IPath
1624      */
1625     public static IPath makeRelativeTo(IPath target, IPath base) {
1626         //can't make relative if devices are not equal
1627         if (target.getDevice() != base.getDevice() && (target.getDevice() == null ||
1628                 !target.getDevice().equalsIgnoreCase(base.getDevice())))
1629             return target;
1630         int commonLength = target.matchingFirstSegments(base);
1631         final int differenceLength = base.segmentCount() - commonLength;
1632         final int newSegmentLength = differenceLength + target.segmentCount() - commonLength;
1633         if (newSegmentLength == 0)
1634             return Path.EMPTY;
1635         String[] newSegments = new String[newSegmentLength];
1636         //add parent references for each segment different from the base
1637         Arrays.fill(newSegments, 0, differenceLength, ".."); //$NON-NLS-1$
1638         //append the segments of this path not in common with the base
1639         System.arraycopy(target.segments(), commonLength, newSegments,
1640                 differenceLength, newSegmentLength - differenceLength);
1641
1642         StringBuilder sb = new StringBuilder();
1643         for (String s : newSegments) {
1644             sb.append(s).append('/');
1645         }
1646
1647         return new Path(null, sb.toString());
1648     }
1649 }
1650