• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onProjectTargetChange(IProject changedProject)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          */
onTargetLoaded(IAndroidTarget target)141         void onTargetLoaded(IAndroidTarget target);
142 
143         /**
144          * Called when the base content of the SDK is parsed.
145          */
onSdkLoaded()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          */
getProject()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          */
reload()166         public abstract void reload();
167 
onProjectTargetChange(IProject changedProject)168         public void onProjectTargetChange(IProject changedProject) {
169             if (changedProject != null && changedProject.equals(getProject())) {
170                 reload();
171             }
172         }
173 
onTargetLoaded(IAndroidTarget target)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 
onSdkLoaded()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      */
getLock()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      */
loadSdk(String sdkLocation)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      */
getCurrent()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      */
getSdkLocation()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      */
getDocumentationBaseUrl()272     public String getDocumentationBaseUrl() {
273         return mDocBaseUrl;
274     }
275 
276     /**
277      * Returns the list of targets that are available in the SDK.
278      */
getTargets()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      */
getTargetFromHashString(String hash)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      */
initProject(IProject project, IAndroidTarget target)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      */
getProjectState(IProject project)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      */
getTarget(IProject project)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      */
loadTarget(ProjectState state)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      */
checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project)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      */
getTargetData(IAndroidTarget target)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      */
getTargetData(IProject project)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      */
getAvdManager()543     public AvdManager getAvdManager() {
544         return mAvdManager;
545     }
546 
getDeviceVersion(IDevice device)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 
getLayoutDeviceManager()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      */
getMainProjectsFor(IProject project)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 
Sdk(SdkManager manager, AvdManager avdManager)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      */
dispose()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 
setTargetData(IAndroidTarget target, AndroidTargetData data)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      */
getDocumentationBaseUrl(String osDocsPath)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      */
loadLayoutDevices()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
toString()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      */
queueLinkUpdateBundle(LinkUpdateBundle bundle)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      */
fillProjectDependenciesList(ProjectState projectState, ArrayList<IProject> libraries)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      */
setupLibraryProject(IProject libProject)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      */
disposeLibraryProject(IProject project)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      */
disposeLibraryProject(String projectName)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      */
getLibraryVariableName(String name)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      */
updateLibraryLinks(LinkUpdateBundle bundle, IProgressMonitor monitor)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 
isRemovedLibrary(LinkUpdateBundle bundle, String libName)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      */
getLinkBundle(ProjectState project, IProject[] oldLibraries)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      */
removeFromList(List<IProject> projects, String name)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      */
findClasspathEntryMatch(ArrayList<IClasspathEntry> cpeList, IPath rawPath, IPath path)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      */
updateProjectsWithNewLibraries(List<ProjectState> libraries)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      */
makeRelativeTo(IPath target, IPath base)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