• 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.common.rendering.LayoutLibrary;
21 import com.android.ide.common.sdk.LoadStatus;
22 import com.android.ide.eclipse.adt.AdtConstants;
23 import com.android.ide.eclipse.adt.AdtPlugin;
24 import com.android.ide.eclipse.adt.internal.build.DexWrapper;
25 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer;
26 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
27 import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
29 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
30 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
31 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
32 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
33 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
34 import com.android.io.StreamException;
35 import com.android.prefs.AndroidLocation.AndroidLocationException;
36 import com.android.sdklib.AndroidVersion;
37 import com.android.sdklib.IAndroidTarget;
38 import com.android.sdklib.ISdkLog;
39 import com.android.sdklib.SdkConstants;
40 import com.android.sdklib.SdkManager;
41 import com.android.sdklib.internal.avd.AvdManager;
42 import com.android.sdklib.internal.project.ProjectProperties;
43 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
44 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
45 
46 import org.eclipse.core.resources.IFile;
47 import org.eclipse.core.resources.IMarkerDelta;
48 import org.eclipse.core.resources.IProject;
49 import org.eclipse.core.resources.IResourceDelta;
50 import org.eclipse.core.resources.IncrementalProjectBuilder;
51 import org.eclipse.core.runtime.CoreException;
52 import org.eclipse.core.runtime.IPath;
53 import org.eclipse.core.runtime.IProgressMonitor;
54 import org.eclipse.core.runtime.IStatus;
55 import org.eclipse.core.runtime.Status;
56 import org.eclipse.core.runtime.jobs.Job;
57 import org.eclipse.jdt.core.IJavaProject;
58 import org.eclipse.jdt.core.JavaCore;
59 
60 import java.io.File;
61 import java.io.IOException;
62 import java.net.MalformedURLException;
63 import java.net.URL;
64 import java.util.ArrayList;
65 import java.util.HashMap;
66 import java.util.HashSet;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Set;
70 import java.util.Map.Entry;
71 
72 /**
73  * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
74  * at the same time.
75  *
76  * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
77  * the Sdk object.
78  *
79  * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
80  */
81 public final class Sdk  {
82     private final static boolean DEBUG = false;
83 
84     private final static Object LOCK = new Object();
85 
86     private static Sdk sCurrentSdk = null;
87 
88     /**
89      * Map associating {@link IProject} and their state {@link ProjectState}.
90      * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
91      */
92     private final static HashMap<IProject, ProjectState> sProjectStateMap =
93             new HashMap<IProject, ProjectState>();
94 
95     /**
96      * Data bundled using during the load of Target data.
97      * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
98      * to compile before the loading was finished. Those projects will be recompiled
99      * at the end of the loading.
100      */
101     private final static class TargetLoadBundle {
102         LoadStatus status;
103         final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>();
104     }
105 
106     private final SdkManager mManager;
107     private final DexWrapper mDexWrapper;
108     private final AvdManager mAvdManager;
109 
110     /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
111     private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
112         new HashMap<IAndroidTarget, AndroidTargetData>();
113     /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
114     private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
115         new HashMap<IAndroidTarget, TargetLoadBundle>();
116 
117     /**
118      * If true the target data will never load anymore. The only way to reload them is to
119      * completely reload the SDK with {@link #loadSdk(String)}
120      */
121     private boolean mDontLoadTargetData = false;
122 
123     private final String mDocBaseUrl;
124 
125     private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager();
126 
127     /**
128      * Classes implementing this interface will receive notification when targets are changed.
129      */
130     public interface ITargetChangeListener {
131         /**
132          * Sent when project has its target changed.
133          */
onProjectTargetChange(IProject changedProject)134         void onProjectTargetChange(IProject changedProject);
135 
136         /**
137          * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
138          * or the SDK is changed).
139          */
onTargetLoaded(IAndroidTarget target)140         void onTargetLoaded(IAndroidTarget target);
141 
142         /**
143          * Called when the base content of the SDK is parsed.
144          */
onSdkLoaded()145         void onSdkLoaded();
146     }
147 
148     /**
149      * Basic abstract implementation of the ITargetChangeListener for the case where both
150      * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
151      * use the same code based on a simple test requiring to know the current IProject.
152      */
153     public static abstract class TargetChangeListener implements ITargetChangeListener {
154         /**
155          * Returns the {@link IProject} associated with the listener.
156          */
getProject()157         public abstract IProject getProject();
158 
159         /**
160          * Called when the listener needs to take action on the event. This is only called
161          * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
162          * match the values received in {@link #onProjectTargetChange(IProject)} and
163          * {@link #onTargetLoaded(IAndroidTarget)}.
164          */
reload()165         public abstract void reload();
166 
onProjectTargetChange(IProject changedProject)167         public void onProjectTargetChange(IProject changedProject) {
168             if (changedProject != null && changedProject.equals(getProject())) {
169                 reload();
170             }
171         }
172 
onTargetLoaded(IAndroidTarget target)173         public void onTargetLoaded(IAndroidTarget target) {
174             IProject project = getProject();
175             if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
176                 reload();
177             }
178         }
179 
onSdkLoaded()180         public void onSdkLoaded() {
181             // do nothing;
182         }
183     }
184 
185     /**
186      * Returns the lock object used to synchronize all operations dealing with SDK, targets and
187      * projects.
188      */
getLock()189     public static final Object getLock() {
190         return LOCK;
191     }
192 
193     /**
194      * Loads an SDK and returns an {@link Sdk} object if success.
195      * <p/>If the SDK failed to load, it displays an error to the user.
196      * @param sdkLocation the OS path to the SDK.
197      */
loadSdk(String sdkLocation)198     public static Sdk loadSdk(String sdkLocation) {
199         synchronized (LOCK) {
200             if (sCurrentSdk != null) {
201                 sCurrentSdk.dispose();
202                 sCurrentSdk = null;
203             }
204 
205             final ArrayList<String> logMessages = new ArrayList<String>();
206             ISdkLog log = new ISdkLog() {
207                 public void error(Throwable throwable, String errorFormat, Object... arg) {
208                     if (errorFormat != null) {
209                         logMessages.add(String.format("Error: " + errorFormat, arg));
210                     }
211 
212                     if (throwable != null) {
213                         logMessages.add(throwable.getMessage());
214                     }
215                 }
216 
217                 public void warning(String warningFormat, Object... arg) {
218                     logMessages.add(String.format("Warning: " + warningFormat, arg));
219                 }
220 
221                 public void printf(String msgFormat, Object... arg) {
222                     logMessages.add(String.format(msgFormat, arg));
223                 }
224             };
225 
226             // get an SdkManager object for the location
227             SdkManager manager = SdkManager.createManager(sdkLocation, log);
228             if (manager != null) {
229                 // load DX.
230                 DexWrapper dexWrapper = new DexWrapper();
231                 String dexLocation =
232                         sdkLocation + File.separator +
233                         SdkConstants.OS_SDK_PLATFORM_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR;
234                 IStatus res = dexWrapper.loadDex(dexLocation);
235                 if (res != Status.OK_STATUS) {
236                     log.error(null, res.getMessage());
237                     dexWrapper = null;
238                 }
239 
240                 // create the AVD Manager
241                 AvdManager avdManager = null;
242                 try {
243                     avdManager = new AvdManager(manager, log);
244                 } catch (AndroidLocationException e) {
245                     log.error(e, "Error parsing the AVDs");
246                 }
247                 sCurrentSdk = new Sdk(manager, dexWrapper, avdManager);
248                 return sCurrentSdk;
249             } else {
250                 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n");
251                 for (String msg : logMessages) {
252                     sb.append('\n');
253                     sb.append(msg);
254                 }
255                 AdtPlugin.displayError("Android SDK", sb.toString());
256             }
257             return null;
258         }
259     }
260 
261     /**
262      * Returns the current {@link Sdk} object.
263      */
getCurrent()264     public static Sdk getCurrent() {
265         synchronized (LOCK) {
266             return sCurrentSdk;
267         }
268     }
269 
270     /**
271      * Returns the location (OS path) of the current SDK.
272      */
getSdkLocation()273     public String getSdkLocation() {
274         return mManager.getLocation();
275     }
276 
277     /**
278      * Returns the URL to the local documentation.
279      * Can return null if no documentation is found in the current SDK.
280      *
281      * @return A file:// URL on the local documentation folder if it exists or null.
282      */
getDocumentationBaseUrl()283     public String getDocumentationBaseUrl() {
284         return mDocBaseUrl;
285     }
286 
287     /**
288      * Returns the list of targets that are available in the SDK.
289      */
getTargets()290     public IAndroidTarget[] getTargets() {
291         return mManager.getTargets();
292     }
293 
294     /**
295      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
296      *
297      * @param hash the {@link IAndroidTarget} hash string.
298      * @return The matching {@link IAndroidTarget} or null.
299      */
getTargetFromHashString(String hash)300     public IAndroidTarget getTargetFromHashString(String hash) {
301         return mManager.getTargetFromHashString(hash);
302     }
303 
304     /**
305      * Initializes a new project with a target. This creates the <code>project.properties</code>
306      * file.
307      * @param project the project to intialize
308      * @param target the project's target.
309      * @throws IOException if creating the file failed in any way.
310      * @throws StreamException
311      */
initProject(IProject project, IAndroidTarget target)312     public void initProject(IProject project, IAndroidTarget target)
313             throws IOException, StreamException {
314         if (project == null || target == null) {
315             return;
316         }
317 
318         synchronized (LOCK) {
319             // check if there's already a state?
320             ProjectState state = getProjectState(project);
321 
322             ProjectPropertiesWorkingCopy properties = null;
323 
324             if (state != null) {
325                 properties = state.getProperties().makeWorkingCopy();
326             }
327 
328             if (properties == null) {
329                 IPath location = project.getLocation();
330                 if (location == null) {  // can return null when the project is being deleted.
331                     // do nothing and return null;
332                     return;
333                 }
334 
335                 properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT);
336             }
337 
338             // save the target hash string in the project persistent property
339             properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
340             properties.save();
341         }
342     }
343 
344     /**
345      * Returns the {@link ProjectState} object associated with a given project.
346      * <p/>
347      * This method is the only way to properly get the project's {@link ProjectState}
348      * If the project has not yet been loaded, then it is loaded.
349      * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
350      * objects, and therefore is static.
351      * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
352      * are replaced.
353      * @param project the request project
354      * @return the ProjectState for the project.
355      */
356     @SuppressWarnings("deprecation")
getProjectState(IProject project)357     public static ProjectState getProjectState(IProject project) {
358         if (project == null) {
359             return null;
360         }
361 
362         synchronized (LOCK) {
363             ProjectState state = sProjectStateMap.get(project);
364             if (state == null) {
365                 // load the project.properties from the project folder.
366                 IPath location = project.getLocation();
367                 if (location == null) {  // can return null when the project is being deleted.
368                     // do nothing and return null;
369                     return null;
370                 }
371 
372                 String projectLocation = location.toOSString();
373 
374                 ProjectProperties properties = ProjectProperties.load(projectLocation,
375                         PropertyType.PROJECT);
376                 if (properties == null) {
377                     // legacy support: look for default.properties and rename it if needed.
378                     properties = ProjectProperties.load(projectLocation,
379                             PropertyType.LEGACY_DEFAULT);
380 
381                     if (properties == null) {
382                         AdtPlugin.log(IStatus.ERROR,
383                                 "Failed to load properties file for project '%s'",
384                                 project.getName());
385                         return null;
386                     } else {
387                         //legacy mode.
388                         // get a working copy with the new type "project"
389                         ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy(
390                                 PropertyType.PROJECT);
391                         // and save it
392                         try {
393                             wc.save();
394 
395                             // delete the old file.
396                             ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT);
397                         } catch (Exception e) {
398                             AdtPlugin.log(IStatus.ERROR,
399                                     "Failed to rename properties file to %1$s for project '%s2$'",
400                                     PropertyType.PROJECT.getFilename(), project.getName());
401                         }
402                     }
403                 }
404 
405                 state = new ProjectState(project, properties);
406                 sProjectStateMap.put(project, state);
407 
408                 // try to resolve the target
409                 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
410                     sCurrentSdk.loadTarget(state);
411                 }
412             }
413 
414             return state;
415         }
416     }
417 
418     /**
419      * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
420      */
getTarget(IProject project)421     public IAndroidTarget getTarget(IProject project) {
422         if (project == null) {
423             return null;
424         }
425 
426         ProjectState state = getProjectState(project);
427         if (state != null) {
428             return state.getTarget();
429         }
430 
431         return null;
432     }
433 
434     /**
435      * Loads the {@link IAndroidTarget} for a given project.
436      * <p/>This method will get the target hash string from the project properties, and resolve
437      * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
438      * @param state the state representing the project to load.
439      * @return the target that was loaded.
440      */
loadTarget(ProjectState state)441     public IAndroidTarget loadTarget(ProjectState state) {
442         IAndroidTarget target = null;
443         if (state != null) {
444             String hash = state.getTargetHashString();
445             if (hash != null) {
446                 state.setTarget(target = getTargetFromHashString(hash));
447             }
448         }
449 
450         return target;
451     }
452 
453     /**
454      * Checks and loads (if needed) the data for a given target.
455      * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
456      * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
457      * <p/>An optional project as second parameter can be given to be recompiled once the target
458      * data is finished loading.
459      * <p/>The return value is non-null only if the target data has already been loaded (and in this
460      * case is the status of the load operation)
461      * @param target the target to load.
462      * @param project an optional project to be recompiled when the target data is loaded.
463      * If the target is already loaded, nothing happens.
464      * @return The load status if the target data is already loaded.
465      */
checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project)466     public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
467         boolean loadData = false;
468 
469         synchronized (LOCK) {
470             if (mDontLoadTargetData) {
471                 return LoadStatus.FAILED;
472             }
473 
474             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
475             if (bundle == null) {
476                 bundle = new TargetLoadBundle();
477                 mTargetDataStatusMap.put(target,bundle);
478 
479                 // set status to loading
480                 bundle.status = LoadStatus.LOADING;
481 
482                 // add project to bundle
483                 if (project != null) {
484                     bundle.projecsToReload.add(project);
485                 }
486 
487                 // and set the flag to start the loading below
488                 loadData = true;
489             } else if (bundle.status == LoadStatus.LOADING) {
490                 // add project to bundle
491                 if (project != null) {
492                     bundle.projecsToReload.add(project);
493                 }
494 
495                 return bundle.status;
496             } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
497                 return bundle.status;
498             }
499         }
500 
501         if (loadData) {
502             Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
503                 @Override
504                 protected IStatus run(IProgressMonitor monitor) {
505                     AdtPlugin plugin = AdtPlugin.getDefault();
506                     try {
507                         IStatus status = new AndroidTargetParser(target).run(monitor);
508 
509                         IJavaProject[] javaProjectArray = null;
510 
511                         synchronized (LOCK) {
512                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
513 
514                             if (status.getCode() != IStatus.OK) {
515                                 bundle.status = LoadStatus.FAILED;
516                                 bundle.projecsToReload.clear();
517                             } else {
518                                 bundle.status = LoadStatus.LOADED;
519 
520                                 // Prepare the array of project to recompile.
521                                 // The call is done outside of the synchronized block.
522                                 javaProjectArray = bundle.projecsToReload.toArray(
523                                         new IJavaProject[bundle.projecsToReload.size()]);
524 
525                                 // and update the UI of the editors that depend on the target data.
526                                 plugin.updateTargetListeners(target);
527                             }
528                         }
529 
530                         if (javaProjectArray != null) {
531                             AndroidClasspathContainerInitializer.updateProjects(javaProjectArray);
532                         }
533 
534                         return status;
535                     } catch (Throwable t) {
536                         synchronized (LOCK) {
537                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
538                             bundle.status = LoadStatus.FAILED;
539                         }
540 
541                         AdtPlugin.log(t, "Exception in checkAndLoadTargetData.");    //$NON-NLS-1$
542                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
543                                 String.format(
544                                         "Parsing Data for %1$s failed", //$NON-NLS-1$
545                                         target.hashString()),
546                                 t);
547                     }
548                 }
549             };
550             job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
551             job.schedule();
552         }
553 
554         // The only way to go through here is when the loading starts through the Job.
555         // Therefore the current status of the target is LOADING.
556         return LoadStatus.LOADING;
557     }
558 
559     /**
560      * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
561      */
getTargetData(IAndroidTarget target)562     public AndroidTargetData getTargetData(IAndroidTarget target) {
563         synchronized (LOCK) {
564             return mTargetDataMap.get(target);
565         }
566     }
567 
568     /**
569      * Return the {@link AndroidTargetData} for a given {@link IProject}.
570      */
getTargetData(IProject project)571     public AndroidTargetData getTargetData(IProject project) {
572         synchronized (LOCK) {
573             IAndroidTarget target = getTarget(project);
574             if (target != null) {
575                 return getTargetData(target);
576             }
577         }
578 
579         return null;
580     }
581 
582     /**
583      * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not
584      * loaded properly, then this will return <code>null</code>.
585      */
getDexWrapper()586     public DexWrapper getDexWrapper() {
587         return mDexWrapper;
588     }
589 
590     /**
591      * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
592      * be <code>null</code>.
593      */
getAvdManager()594     public AvdManager getAvdManager() {
595         return mAvdManager;
596     }
597 
getDeviceVersion(IDevice device)598     public static AndroidVersion getDeviceVersion(IDevice device) {
599         try {
600             Map<String, String> props = device.getProperties();
601             String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
602             if (apiLevel == null) {
603                 return null;
604             }
605 
606             return new AndroidVersion(Integer.parseInt(apiLevel),
607                     props.get((IDevice.PROP_BUILD_CODENAME)));
608         } catch (NumberFormatException e) {
609             return null;
610         }
611     }
612 
getLayoutDeviceManager()613     public LayoutDeviceManager getLayoutDeviceManager() {
614         return mLayoutDeviceManager;
615     }
616 
617     /**
618      * Returns a list of {@link ProjectState} representing projects depending, directly or
619      * indirectly on a given library project.
620      * @param project the library project.
621      * @return a possibly empty list of ProjectState.
622      */
getMainProjectsFor(IProject project)623     public static Set<ProjectState> getMainProjectsFor(IProject project) {
624         synchronized (LOCK) {
625             // first get the project directly depending on this.
626             HashSet<ProjectState> list = new HashSet<ProjectState>();
627 
628             // loop on all project and see if ProjectState.getLibrary returns a non null
629             // project.
630             for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
631                 if (project != entry.getKey()) {
632                     LibraryState library = entry.getValue().getLibrary(project);
633                     if (library != null) {
634                         list.add(entry.getValue());
635                     }
636                 }
637             }
638 
639             // now look for projects depending on the projects directly depending on the library.
640             HashSet<ProjectState> result = new HashSet<ProjectState>(list);
641             for (ProjectState p : list) {
642                 if (p.isLibrary()) {
643                     Set<ProjectState> set = getMainProjectsFor(p.getProject());
644                     result.addAll(set);
645                 }
646             }
647 
648             return result;
649         }
650     }
651 
652     /**
653      * Unload the SDK's target data.
654      *
655      * If <var>preventReload</var>, this effect is final until the SDK instance is changed
656      * through {@link #loadSdk(String)}.
657      *
658      * The goal is to unload the targets to be able to replace existing targets with new ones,
659      * before calling {@link #loadSdk(String)} to fully reload the SDK.
660      *
661      * @param preventReload prevent the data from being loaded again for the remaining live of
662      *   this {@link Sdk} instance.
663      */
unloadTargetData(boolean preventReload)664     public void unloadTargetData(boolean preventReload) {
665         synchronized (LOCK) {
666             mDontLoadTargetData = preventReload;
667 
668             // dispose of the target data.
669             for (AndroidTargetData data : mTargetDataMap.values()) {
670                 data.dispose();
671             }
672 
673             mTargetDataMap.clear();
674         }
675     }
676 
Sdk(SdkManager manager, DexWrapper dexWrapper, AvdManager avdManager)677     private Sdk(SdkManager manager, DexWrapper dexWrapper, AvdManager avdManager) {
678         mManager = manager;
679         mDexWrapper = dexWrapper;
680         mAvdManager = avdManager;
681 
682         // listen to projects closing
683         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
684         // need to register the resource event listener first because the project listener
685         // is called back during registration with project opened in the workspace.
686         monitor.addResourceEventListener(mResourceEventListener);
687         monitor.addProjectListener(mProjectListener);
688         monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED);
689 
690         // pre-compute some paths
691         mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() +
692                 SdkConstants.OS_SDK_DOCS_FOLDER);
693 
694         // load the built-in and user layout devices
695         mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation());
696         // and the ones from the add-on
697         loadLayoutDevices();
698 
699         // update whatever ProjectState is already present with new IAndroidTarget objects.
700         synchronized (LOCK) {
701             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
702                 entry.getValue().setTarget(
703                         getTargetFromHashString(entry.getValue().getTargetHashString()));
704             }
705         }
706     }
707 
708     /**
709      *  Cleans and unloads the SDK.
710      */
dispose()711     private void dispose() {
712         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
713         monitor.removeProjectListener(mProjectListener);
714         monitor.removeFileListener(mFileListener);
715         monitor.removeResourceEventListener(mResourceEventListener);
716 
717         // the IAndroidTarget objects are now obsolete so update the project states.
718         synchronized (LOCK) {
719             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
720                 entry.getValue().setTarget(null);
721             }
722 
723             // dispose of the target data.
724             for (AndroidTargetData data : mTargetDataMap.values()) {
725                 data.dispose();
726             }
727 
728             mTargetDataMap.clear();
729         }
730     }
731 
setTargetData(IAndroidTarget target, AndroidTargetData data)732     void setTargetData(IAndroidTarget target, AndroidTargetData data) {
733         synchronized (LOCK) {
734             mTargetDataMap.put(target, data);
735         }
736     }
737 
738     /**
739      * Returns the URL to the local documentation.
740      * Can return null if no documentation is found in the current SDK.
741      *
742      * @param osDocsPath Path to the documentation folder in the current SDK.
743      *  The folder may not actually exist.
744      * @return A file:// URL on the local documentation folder if it exists or null.
745      */
getDocumentationBaseUrl(String osDocsPath)746     private String getDocumentationBaseUrl(String osDocsPath) {
747         File f = new File(osDocsPath);
748 
749         if (f.isDirectory()) {
750             try {
751                 // Note: to create a file:// URL, one would typically use something like
752                 // f.toURI().toURL().toString(). However this generates a broken path on
753                 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
754                 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
755                 // do the correct thing manually.
756 
757                 String path = f.getAbsolutePath();
758                 if (File.separatorChar != '/') {
759                     path = path.replace(File.separatorChar, '/');
760                 }
761 
762                 // For some reason the URL class doesn't add the mandatory "//" after
763                 // the "file:" protocol name, so it has to be hacked into the path.
764                 URL url = new URL("file", null, "//" + path);  //$NON-NLS-1$ //$NON-NLS-2$
765                 String result = url.toString();
766                 return result;
767             } catch (MalformedURLException e) {
768                 // ignore malformed URLs
769             }
770         }
771 
772         return null;
773     }
774 
775     /**
776      * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to
777      * load {@link LayoutDevice} from them.
778      */
loadLayoutDevices()779     private void loadLayoutDevices() {
780         IAndroidTarget[] targets = mManager.getTargets();
781         for (IAndroidTarget target : targets) {
782             if (target.isPlatform() == false) {
783                 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML);
784                 if (deviceXml.isFile()) {
785                     mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml);
786                 }
787             }
788         }
789 
790         mLayoutDeviceManager.sealAddonLayoutDevices();
791     }
792 
793     /**
794      * Delegate listener for project changes.
795      */
796     private IProjectListener mProjectListener = new IProjectListener() {
797         public void projectClosed(IProject project) {
798             onProjectRemoved(project, false /*deleted*/);
799         }
800 
801         public void projectDeleted(IProject project) {
802             onProjectRemoved(project, true /*deleted*/);
803         }
804 
805         private void onProjectRemoved(IProject removedProject, boolean deleted) {
806             try {
807                 if (removedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
808                     return;
809                 }
810             } catch (CoreException e) {
811                 // this can only happen if the project does not exist or is not open, neither
812                 // of which can happen here since we're processing a Project removed/deleted event
813                 // which is processed before the project is actually removed/closed.
814             }
815 
816             if (DEBUG) {
817                 System.out.println(">>> CLOSED: " + removedProject.getName());
818             }
819 
820             // get the target project
821             synchronized (LOCK) {
822                 // Don't use getProject() as it could create the ProjectState if it's not
823                 // there yet and this is not what we want. We want the current object.
824                 // Therefore, direct access to the map.
825                 ProjectState removedState = sProjectStateMap.get(removedProject);
826                 if (removedState != null) {
827                     // 1. clear the layout lib cache associated with this project
828                     IAndroidTarget target = removedState.getTarget();
829                     if (target != null) {
830                         // get the bridge for the target, and clear the cache for this project.
831                         AndroidTargetData data = mTargetDataMap.get(target);
832                         if (data != null) {
833                             LayoutLibrary layoutLib = data.getLayoutLibrary();
834                             if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) {
835                                 layoutLib.clearCaches(removedProject);
836                             }
837                         }
838                     }
839 
840                     // 2. if the project is a library, make sure to update the
841                     // LibraryState for any project referencing it.
842                     // Also, record the updated projects that are libraries, to update
843                     // projects that depend on them.
844                     for (ProjectState projectState : sProjectStateMap.values()) {
845                         LibraryState libState = projectState.getLibrary(removedProject);
846                         if (libState != null) {
847                             // Close the library right away.
848                             // This remove links between the LibraryState and the projectState.
849                             // This is because in case of a rename of a project, projectClosed and
850                             // projectOpened will be called before any other job is run, so we
851                             // need to make sure projectOpened is closed with the main project
852                             // state up to date.
853                             libState.close();
854 
855                             // record that this project changed, and in case it's a library
856                             // that its parents need to be updated as well.
857                             markProject(projectState, projectState.isLibrary());
858                         }
859                     }
860 
861                     // now remove the project for the project map.
862                     sProjectStateMap.remove(removedProject);
863                 }
864             }
865 
866             if (DEBUG) {
867                 System.out.println("<<<");
868             }
869         }
870 
871         public void projectOpened(IProject project) {
872             onProjectOpened(project);
873         }
874 
875         public void projectOpenedWithWorkspace(IProject project) {
876             // no need to force recompilation when projects are opened with the workspace.
877             onProjectOpened(project);
878         }
879 
880         private void onProjectOpened(final IProject openedProject) {
881             try {
882                 if (openedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
883                     return;
884                 }
885             } catch (CoreException e) {
886                 // this can only happen if the project does not exist or is not open, neither
887                 // of which can happen here since we're processing a Project opened event.
888             }
889 
890 
891             ProjectState openedState = getProjectState(openedProject);
892             if (openedState != null) {
893                 if (DEBUG) {
894                     System.out.println(">>> OPENED: " + openedProject.getName());
895                 }
896 
897                 synchronized (LOCK) {
898                     final boolean isLibrary = openedState.isLibrary();
899                     final boolean hasLibraries = openedState.hasLibraries();
900 
901                     if (isLibrary || hasLibraries) {
902                         boolean foundLibraries = false;
903                         // loop on all the existing project and update them based on this new
904                         // project
905                         for (ProjectState projectState : sProjectStateMap.values()) {
906                             if (projectState != openedState) {
907                                 // If the project has libraries, check if this project
908                                 // is a reference.
909                                 if (hasLibraries) {
910                                     // ProjectState#needs() both checks if this is a missing library
911                                     // and updates LibraryState to contains the new values.
912                                     // This must always be called.
913                                     LibraryState libState = openedState.needs(projectState);
914 
915                                     if (libState != null) {
916                                         // found a library! Add the main project to the list of
917                                         // modified project
918                                         foundLibraries = true;
919                                     }
920                                 }
921 
922                                 // if the project is a library check if the other project depend
923                                 // on it.
924                                 if (isLibrary) {
925                                     // ProjectState#needs() both checks if this is a missing library
926                                     // and updates LibraryState to contains the new values.
927                                     // This must always be called.
928                                     LibraryState libState = projectState.needs(openedState);
929 
930                                     if (libState != null) {
931                                         // There's a dependency! Add the project to the list of
932                                         // modified project, but also to a list of projects
933                                         // that saw one of its dependencies resolved.
934                                         markProject(projectState, projectState.isLibrary());
935                                     }
936                                 }
937                             }
938                         }
939 
940                         // if the project has a libraries and we found at least one, we add
941                         // the project to the list of modified project.
942                         // Since we already went through the parent, no need to update them.
943                         if (foundLibraries) {
944                             markProject(openedState, false /*updateParents*/);
945                         }
946                     }
947                 }
948 
949                 if (DEBUG) {
950                     System.out.println("<<<");
951                 }
952             }
953         }
954 
955         public void projectRenamed(IProject project, IPath from) {
956             // we don't actually care about this anymore.
957         }
958     };
959 
960     /**
961      * Delegate listener for file changes.
962      */
963     private IFileListener mFileListener = new IFileListener() {
964         public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) {
965             if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) &&
966                     file.getParent() == file.getProject()) {
967                 try {
968                     // reload the content of the project.properties file and update
969                     // the target.
970                     IProject iProject = file.getProject();
971 
972                     if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
973                         return;
974                     }
975 
976                     ProjectState state = Sdk.getProjectState(iProject);
977 
978                     // get the current target
979                     IAndroidTarget oldTarget = state.getTarget();
980 
981                     // get the current library flag
982                     boolean wasLibrary = state.isLibrary();
983 
984                     LibraryDifference diff = state.reloadProperties();
985 
986                     // load the (possibly new) target.
987                     IAndroidTarget newTarget = loadTarget(state);
988 
989                     // reload the libraries if needed
990                     if (diff.hasDiff()) {
991                         if (diff.added) {
992                             synchronized (LOCK) {
993                                 for (ProjectState projectState : sProjectStateMap.values()) {
994                                     if (projectState != state) {
995                                         // need to call needs to do the libraryState link,
996                                         // but no need to look at the result, as we'll compare
997                                         // the result of getFullLibraryProjects()
998                                         // this is easier to due to indirect dependencies.
999                                         state.needs(projectState);
1000                                     }
1001                                 }
1002                             }
1003                         }
1004 
1005                         markProject(state, wasLibrary || state.isLibrary());
1006                     }
1007 
1008                     // apply the new target if needed.
1009                     if (newTarget != oldTarget) {
1010                         IJavaProject javaProject = BaseProjectHelper.getJavaProject(
1011                                 file.getProject());
1012                         if (javaProject != null) {
1013                             AndroidClasspathContainerInitializer.updateProjects(
1014                                     new IJavaProject[] { javaProject });
1015                         }
1016 
1017                         // update the editors to reload with the new target
1018                         AdtPlugin.getDefault().updateTargetListeners(iProject);
1019                     }
1020                 } catch (CoreException e) {
1021                     // This can't happen as it's only for closed project (or non existing)
1022                     // but in that case we can't get a fileChanged on this file.
1023                 }
1024             }
1025         }
1026     };
1027 
1028     /** List of modified projects. This is filled in
1029      * {@link IProjectListener#projectOpened(IProject)},
1030      * {@link IProjectListener#projectOpenedWithWorkspace(IProject)},
1031      * {@link IProjectListener#projectClosed(IProject)}, and
1032      * {@link IProjectListener#projectDeleted(IProject)} and processed in
1033      * {@link IResourceEventListener#resourceChangeEventEnd()}.
1034      */
1035     private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>();
1036     private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>();
1037 
markProject(ProjectState projectState, boolean updateParents)1038     private void markProject(ProjectState projectState, boolean updateParents) {
1039         if (mModifiedProjects.contains(projectState) == false) {
1040             if (DEBUG) {
1041                 System.out.println("\tMARKED: " + projectState.getProject().getName());
1042             }
1043             mModifiedProjects.add(projectState);
1044         }
1045 
1046         // if the project is resolved also add it to this list.
1047         if (updateParents) {
1048             if (mModifiedChildProjects.contains(projectState) == false) {
1049                 if (DEBUG) {
1050                     System.out.println("\tMARKED(child): " + projectState.getProject().getName());
1051                 }
1052                 mModifiedChildProjects.add(projectState);
1053             }
1054         }
1055     }
1056 
1057     /**
1058      * Delegate listener for resource changes. This is called before and after any calls to the
1059      * project and file listeners (for a given resource change event).
1060      */
1061     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
1062         public void resourceChangeEventStart() {
1063             mModifiedProjects.clear();
1064             mModifiedChildProjects.clear();
1065         }
1066 
1067         public void resourceChangeEventEnd() {
1068             if (mModifiedProjects.size() == 0) {
1069                 return;
1070             }
1071 
1072             // first make sure all the parents are updated
1073             updateParentProjects();
1074 
1075             // for all modified projects, update their library list
1076             // and gather their IProject
1077             final List<IJavaProject> projectList = new ArrayList<IJavaProject>();
1078             for (ProjectState state : mModifiedProjects) {
1079                 state.updateFullLibraryList();
1080                 projectList.add(JavaCore.create(state.getProject()));
1081             }
1082 
1083             Job job = new Job("Android Library Update") { //$NON-NLS-1$
1084                 @Override
1085                 protected IStatus run(IProgressMonitor monitor) {
1086                     LibraryClasspathContainerInitializer.updateProjects(
1087                             projectList.toArray(new IJavaProject[projectList.size()]));
1088 
1089                     for (IJavaProject javaProject : projectList) {
1090                         try {
1091                             javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD,
1092                                     monitor);
1093                         } catch (CoreException e) {
1094                             // pass
1095                         }
1096                     }
1097                     return Status.OK_STATUS;
1098                 }
1099             };
1100             job.setPriority(Job.BUILD);
1101             job.schedule();
1102         }
1103     };
1104 
1105     /**
1106      * Updates all existing projects with a given list of new/updated libraries.
1107      * This loops through all opened projects and check if they depend on any of the given
1108      * library project, and if they do, they are linked together.
1109      * @param libraries the list of new/updated library projects.
1110      */
updateParentProjects()1111     private void updateParentProjects() {
1112         if (mModifiedChildProjects.size() == 0) {
1113             return;
1114         }
1115 
1116         ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects);
1117         mModifiedChildProjects.clear();
1118         synchronized (LOCK) {
1119             // for each project for which we must update its parent, we loop on the parent
1120             // projects and adds them to the list of modified projects. If they are themselves
1121             // libraries, we add them too.
1122             for (ProjectState state : childProjects) {
1123                 if (DEBUG) {
1124                     System.out.println(">>> Updating parents of " + state.getProject().getName());
1125                 }
1126                 List<ProjectState> parents = state.getParentProjects();
1127                 for (ProjectState parent : parents) {
1128                     markProject(parent, parent.isLibrary());
1129                 }
1130                 if (DEBUG) {
1131                     System.out.println("<<<");
1132                 }
1133             }
1134         }
1135 
1136         // done, but there may be parents that are also libraries. Need to update their parents.
1137         updateParentProjects();
1138     }
1139 }
1140 
1141