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 static com.android.SdkConstants.DOT_XML;
20 import static com.android.SdkConstants.EXT_JAR;
21 import static com.android.SdkConstants.FD_RES;
22 
23 import com.android.SdkConstants;
24 import com.android.annotations.NonNull;
25 import com.android.annotations.Nullable;
26 import com.android.ddmlib.IDevice;
27 import com.android.ide.common.rendering.LayoutLibrary;
28 import com.android.ide.common.sdk.LoadStatus;
29 import com.android.ide.eclipse.adt.AdtConstants;
30 import com.android.ide.eclipse.adt.AdtPlugin;
31 import com.android.ide.eclipse.adt.internal.build.DexWrapper;
32 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
33 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
34 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
35 import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
36 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
37 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
38 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
39 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
40 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
41 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
42 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
43 import com.android.io.StreamException;
44 import com.android.prefs.AndroidLocation.AndroidLocationException;
45 import com.android.sdklib.AndroidVersion;
46 import com.android.sdklib.BuildToolInfo;
47 import com.android.sdklib.IAndroidTarget;
48 import com.android.sdklib.SdkManager;
49 import com.android.sdklib.devices.DeviceManager;
50 import com.android.sdklib.internal.avd.AvdManager;
51 import com.android.sdklib.internal.project.ProjectProperties;
52 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
53 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
54 import com.android.sdklib.repository.FullRevision;
55 import com.android.utils.ILogger;
56 import com.google.common.collect.Maps;
57 
58 import org.eclipse.core.resources.IFile;
59 import org.eclipse.core.resources.IFolder;
60 import org.eclipse.core.resources.IMarker;
61 import org.eclipse.core.resources.IMarkerDelta;
62 import org.eclipse.core.resources.IProject;
63 import org.eclipse.core.resources.IResource;
64 import org.eclipse.core.resources.IResourceDelta;
65 import org.eclipse.core.resources.IncrementalProjectBuilder;
66 import org.eclipse.core.resources.ResourcesPlugin;
67 import org.eclipse.core.runtime.CoreException;
68 import org.eclipse.core.runtime.IPath;
69 import org.eclipse.core.runtime.IProgressMonitor;
70 import org.eclipse.core.runtime.IStatus;
71 import org.eclipse.core.runtime.QualifiedName;
72 import org.eclipse.core.runtime.Status;
73 import org.eclipse.core.runtime.jobs.Job;
74 import org.eclipse.jdt.core.IJavaProject;
75 import org.eclipse.jdt.core.JavaCore;
76 import org.eclipse.jdt.core.JavaModelException;
77 import org.eclipse.jface.preference.IPreferenceStore;
78 import org.eclipse.ui.IEditorDescriptor;
79 import org.eclipse.ui.IEditorInput;
80 import org.eclipse.ui.IEditorPart;
81 import org.eclipse.ui.IEditorReference;
82 import org.eclipse.ui.IFileEditorInput;
83 import org.eclipse.ui.IWorkbenchPage;
84 import org.eclipse.ui.IWorkbenchPartSite;
85 import org.eclipse.ui.IWorkbenchWindow;
86 import org.eclipse.ui.PartInitException;
87 import org.eclipse.ui.PlatformUI;
88 import org.eclipse.ui.ide.IDE;
89 
90 import java.io.File;
91 import java.io.IOException;
92 import java.net.MalformedURLException;
93 import java.net.URL;
94 import java.util.ArrayList;
95 import java.util.Arrays;
96 import java.util.Collection;
97 import java.util.HashMap;
98 import java.util.HashSet;
99 import java.util.List;
100 import java.util.Map;
101 import java.util.Map.Entry;
102 import java.util.Set;
103 import java.util.concurrent.atomic.AtomicBoolean;
104 
105 /**
106  * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
107  * at the same time.
108  *
109  * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
110  * the Sdk object.
111  *
112  * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
113  */
114 public final class Sdk  {
115     private final static boolean DEBUG = false;
116 
117     private final static Object LOCK = new Object();
118 
119     private static Sdk sCurrentSdk = null;
120 
121     /**
122      * Map associating {@link IProject} and their state {@link ProjectState}.
123      * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
124      */
125     private final static HashMap<IProject, ProjectState> sProjectStateMap =
126             new HashMap<IProject, ProjectState>();
127 
128     /**
129      * Data bundled using during the load of Target data.
130      * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
131      * to compile before the loading was finished. Those projects will be recompiled
132      * at the end of the loading.
133      */
134     private final static class TargetLoadBundle {
135         LoadStatus status;
136         final HashSet<IJavaProject> projectsToReload = new HashSet<IJavaProject>();
137     }
138 
139     private final SdkManager mManager;
140     private final Map<String, DexWrapper> mDexWrappers = Maps.newHashMap();
141     private final AvdManager mAvdManager;
142     private final DeviceManager mDeviceManager;
143 
144     /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
145     private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
146         new HashMap<IAndroidTarget, AndroidTargetData>();
147     /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
148     private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
149         new HashMap<IAndroidTarget, TargetLoadBundle>();
150 
151     /**
152      * If true the target data will never load anymore. The only way to reload them is to
153      * completely reload the SDK with {@link #loadSdk(String)}
154      */
155     private boolean mDontLoadTargetData = false;
156 
157     private final String mDocBaseUrl;
158 
159     /**
160      * Classes implementing this interface will receive notification when targets are changed.
161      */
162     public interface ITargetChangeListener {
163         /**
164          * Sent when project has its target changed.
165          */
onProjectTargetChange(IProject changedProject)166         void onProjectTargetChange(IProject changedProject);
167 
168         /**
169          * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
170          * or the SDK is changed).
171          */
onTargetLoaded(IAndroidTarget target)172         void onTargetLoaded(IAndroidTarget target);
173 
174         /**
175          * Called when the base content of the SDK is parsed.
176          */
onSdkLoaded()177         void onSdkLoaded();
178     }
179 
180     /**
181      * Basic abstract implementation of the ITargetChangeListener for the case where both
182      * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
183      * use the same code based on a simple test requiring to know the current IProject.
184      */
185     public static abstract class TargetChangeListener implements ITargetChangeListener {
186         /**
187          * Returns the {@link IProject} associated with the listener.
188          */
getProject()189         public abstract IProject getProject();
190 
191         /**
192          * Called when the listener needs to take action on the event. This is only called
193          * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
194          * match the values received in {@link #onProjectTargetChange(IProject)} and
195          * {@link #onTargetLoaded(IAndroidTarget)}.
196          */
reload()197         public abstract void reload();
198 
199         @Override
onProjectTargetChange(IProject changedProject)200         public void onProjectTargetChange(IProject changedProject) {
201             if (changedProject != null && changedProject.equals(getProject())) {
202                 reload();
203             }
204         }
205 
206         @Override
onTargetLoaded(IAndroidTarget target)207         public void onTargetLoaded(IAndroidTarget target) {
208             IProject project = getProject();
209             if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
210                 reload();
211             }
212         }
213 
214         @Override
onSdkLoaded()215         public void onSdkLoaded() {
216             // do nothing;
217         }
218     }
219 
220     /**
221      * Returns the lock object used to synchronize all operations dealing with SDK, targets and
222      * projects.
223      */
224     @NonNull
getLock()225     public static final Object getLock() {
226         return LOCK;
227     }
228 
229     /**
230      * Loads an SDK and returns an {@link Sdk} object if success.
231      * <p/>If the SDK failed to load, it displays an error to the user.
232      * @param sdkLocation the OS path to the SDK.
233      */
234     @Nullable
loadSdk(String sdkLocation)235     public static Sdk loadSdk(String sdkLocation) {
236         synchronized (LOCK) {
237             if (sCurrentSdk != null) {
238                 sCurrentSdk.dispose();
239                 sCurrentSdk = null;
240             }
241 
242             final AtomicBoolean hasWarning = new AtomicBoolean();
243             final AtomicBoolean hasError = new AtomicBoolean();
244             final ArrayList<String> logMessages = new ArrayList<String>();
245             ILogger log = new ILogger() {
246                 @Override
247                 public void error(@Nullable Throwable throwable, @Nullable String errorFormat,
248                         Object... arg) {
249                     hasError.set(true);
250                     if (errorFormat != null) {
251                         logMessages.add(String.format("Error: " + errorFormat, arg));
252                     }
253 
254                     if (throwable != null) {
255                         logMessages.add(throwable.getMessage());
256                     }
257                 }
258 
259                 @Override
260                 public void warning(@NonNull String warningFormat, Object... arg) {
261                     hasWarning.set(true);
262                     logMessages.add(String.format("Warning: " + warningFormat, arg));
263                 }
264 
265                 @Override
266                 public void info(@NonNull String msgFormat, Object... arg) {
267                     logMessages.add(String.format(msgFormat, arg));
268                 }
269 
270                 @Override
271                 public void verbose(@NonNull String msgFormat, Object... arg) {
272                     info(msgFormat, arg);
273                 }
274             };
275 
276             // get an SdkManager object for the location
277             SdkManager manager = SdkManager.createManager(sdkLocation, log);
278             try {
279                 if (manager == null) {
280                     hasError.set(true);
281                 } else {
282                     // create the AVD Manager
283                     AvdManager avdManager = null;
284                     try {
285                         avdManager = AvdManager.getInstance(manager.getLocalSdk(), log);
286                     } catch (AndroidLocationException e) {
287                         log.error(e, "Error parsing the AVDs");
288                     }
289                     sCurrentSdk = new Sdk(manager, avdManager);
290                     return sCurrentSdk;
291                 }
292             } finally {
293                 if (hasError.get() || hasWarning.get()) {
294                     StringBuilder sb = new StringBuilder(
295                             String.format("%s when loading the SDK:\n",
296                                     hasError.get() ? "Error" : "Warning"));
297                     for (String msg : logMessages) {
298                         sb.append('\n');
299                         sb.append(msg);
300                     }
301                     if (hasError.get()) {
302                         AdtPlugin.printErrorToConsole("Android SDK", sb.toString());
303                         AdtPlugin.displayError("Android SDK", sb.toString());
304                     } else {
305                         AdtPlugin.printToConsole("Android SDK", sb.toString());
306                     }
307                 }
308             }
309             return null;
310         }
311     }
312 
313     /**
314      * Returns the current {@link Sdk} object.
315      */
316     @Nullable
getCurrent()317     public static Sdk getCurrent() {
318         synchronized (LOCK) {
319             return sCurrentSdk;
320         }
321     }
322 
323     /**
324      * Returns the location of the current SDK as an OS path string.
325      * Guaranteed to be terminated by a platform-specific path separator.
326      * <p/>
327      * Due to {@link File} canonicalization, this MAY differ from the string used to initialize
328      * the SDK path.
329      *
330      * @return The SDK OS path or null if no SDK is setup.
331      * @deprecated Consider using {@link #getSdkFileLocation()} instead.
332      * @see #getSdkFileLocation()
333      */
334     @Deprecated
335     @Nullable
getSdkOsLocation()336     public String getSdkOsLocation() {
337         String path = mManager == null ? null : mManager.getLocation();
338         if (path != null) {
339             // For backward compatibility make sure it ends with a separator.
340             // This used to be the case when the SDK Manager was created from a String path
341             // but now that a File is internally used the trailing dir separator is lost.
342             if (path.length() > 0 && !path.endsWith(File.separator)) {
343                 path = path + File.separator;
344             }
345         }
346         return path;
347     }
348 
349     /**
350      * Returns the location of the current SDK as a {@link File} or null.
351      *
352      * @return The SDK OS path or null if no SDK is setup.
353      */
354     @Nullable
getSdkFileLocation()355     public File getSdkFileLocation() {
356         if (mManager == null || mManager.getLocalSdk() == null) {
357             return null;
358         }
359         return mManager.getLocalSdk().getLocation();
360     }
361 
362     /**
363      * Returns a <em>new</em> {@link SdkManager} that can parse the SDK located
364      * at the current {@link #getSdkOsLocation()}.
365      * <p/>
366      * Implementation detail: The {@link Sdk} has its own internal manager with
367      * a custom logger which is not designed to be useful for outsiders. Callers
368      * who need their own {@link SdkManager} for parsing will often want to control
369      * the logger for their own need.
370      * <p/>
371      * This is just a convenient method equivalent to writing:
372      * <pre>SdkManager.createManager(Sdk.getCurrent().getSdkLocation(), log);</pre>
373      *
374      * @param log The logger for the {@link SdkManager}.
375      * @return A new {@link SdkManager} parsing the same location.
376      */
getNewSdkManager(@onNull ILogger log)377     public @Nullable SdkManager getNewSdkManager(@NonNull ILogger log) {
378         return SdkManager.createManager(getSdkOsLocation(), log);
379     }
380 
381     /**
382      * Returns the URL to the local documentation.
383      * Can return null if no documentation is found in the current SDK.
384      *
385      * @return A file:// URL on the local documentation folder if it exists or null.
386      */
387     @Nullable
getDocumentationBaseUrl()388     public String getDocumentationBaseUrl() {
389         return mDocBaseUrl;
390     }
391 
392     /**
393      * Returns the list of targets that are available in the SDK.
394      */
getTargets()395     public IAndroidTarget[] getTargets() {
396         return mManager.getTargets();
397     }
398 
399     /**
400      * Queries the underlying SDK Manager to check whether the platforms or addons
401      * directories have changed on-disk. Does not reload the SDK.
402      * <p/>
403      * This is a quick test based on the presence of the directories, their timestamps
404      * and a quick checksum of the source.properties files. It's possible to have
405      * false positives (e.g. if a file is manually modified in a platform) or false
406      * negatives (e.g. if a platform data file is changed manually in a 2nd level
407      * directory without altering the source.properties.)
408      */
haveTargetsChanged()409     public boolean haveTargetsChanged() {
410         return mManager.hasChanged();
411     }
412 
413     /**
414      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
415      *
416      * @param hash the {@link IAndroidTarget} hash string.
417      * @return The matching {@link IAndroidTarget} or null.
418      */
419     @Nullable
getTargetFromHashString(@onNull String hash)420     public IAndroidTarget getTargetFromHashString(@NonNull String hash) {
421         return mManager.getTargetFromHashString(hash);
422     }
423 
424     @Nullable
getBuildToolInfo(@ullable String buildToolVersion)425     public BuildToolInfo getBuildToolInfo(@Nullable String buildToolVersion) {
426         if (buildToolVersion != null) {
427             try {
428                 return mManager.getBuildTool(FullRevision.parseRevision(buildToolVersion));
429             } catch (Exception e) {
430                 // ignore, return null below.
431             }
432         }
433 
434         return null;
435     }
436 
437     @Nullable
getLatestBuildTool()438     public BuildToolInfo getLatestBuildTool() {
439         return mManager.getLatestBuildTool();
440     }
441 
442     /**
443      * Initializes a new project with a target. This creates the <code>project.properties</code>
444      * file.
445      * @param project the project to initialize
446      * @param target the project's target.
447      * @throws IOException if creating the file failed in any way.
448      * @throws StreamException if processing the project property file fails
449      */
initProject(@ullable IProject project, @Nullable IAndroidTarget target)450     public void initProject(@Nullable IProject project, @Nullable IAndroidTarget target)
451             throws IOException, StreamException {
452         if (project == null || target == null) {
453             return;
454         }
455 
456         synchronized (LOCK) {
457             // check if there's already a state?
458             ProjectState state = getProjectState(project);
459 
460             ProjectPropertiesWorkingCopy properties = null;
461 
462             if (state != null) {
463                 properties = state.getProperties().makeWorkingCopy();
464             }
465 
466             if (properties == null) {
467                 IPath location = project.getLocation();
468                 if (location == null) {  // can return null when the project is being deleted.
469                     // do nothing and return null;
470                     return;
471                 }
472 
473                 properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT);
474             }
475 
476             // save the target hash string in the project persistent property
477             properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
478             properties.save();
479         }
480     }
481 
482     /**
483      * Returns the {@link ProjectState} object associated with a given project.
484      * <p/>
485      * This method is the only way to properly get the project's {@link ProjectState}
486      * If the project has not yet been loaded, then it is loaded.
487      * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
488      * objects, and therefore is static.
489      * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
490      * are replaced.
491      * @param project the request project
492      * @return the ProjectState for the project.
493      */
494     @Nullable
495     @SuppressWarnings("deprecation")
getProjectState(IProject project)496     public static ProjectState getProjectState(IProject project) {
497         if (project == null) {
498             return null;
499         }
500 
501         synchronized (LOCK) {
502             ProjectState state = sProjectStateMap.get(project);
503             if (state == null) {
504                 // load the project.properties from the project folder.
505                 IPath location = project.getLocation();
506                 if (location == null) {  // can return null when the project is being deleted.
507                     // do nothing and return null;
508                     return null;
509                 }
510 
511                 String projectLocation = location.toOSString();
512 
513                 ProjectProperties properties = ProjectProperties.load(projectLocation,
514                         PropertyType.PROJECT);
515                 if (properties == null) {
516                     // legacy support: look for default.properties and rename it if needed.
517                     properties = ProjectProperties.load(projectLocation,
518                             PropertyType.LEGACY_DEFAULT);
519 
520                     if (properties == null) {
521                         AdtPlugin.log(IStatus.ERROR,
522                                 "Failed to load properties file for project '%s'",
523                                 project.getName());
524                         return null;
525                     } else {
526                         //legacy mode.
527                         // get a working copy with the new type "project"
528                         ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy(
529                                 PropertyType.PROJECT);
530                         // and save it
531                         try {
532                             wc.save();
533 
534                             // delete the old file.
535                             ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT);
536 
537                             // make sure to use the new properties
538                             properties = ProjectProperties.load(projectLocation,
539                                     PropertyType.PROJECT);
540                         } catch (Exception e) {
541                             AdtPlugin.log(IStatus.ERROR,
542                                     "Failed to rename properties file to %1$s for project '%s2$'",
543                                     PropertyType.PROJECT.getFilename(), project.getName());
544                         }
545                     }
546                 }
547 
548                 state = new ProjectState(project, properties);
549                 sProjectStateMap.put(project, state);
550 
551                 // try to resolve the target
552                 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
553                     sCurrentSdk.loadTargetAndBuildTools(state);
554                 }
555             }
556 
557             return state;
558         }
559     }
560 
561     /**
562      * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
563      */
564     @Nullable
getTarget(IProject project)565     public IAndroidTarget getTarget(IProject project) {
566         if (project == null) {
567             return null;
568         }
569 
570         ProjectState state = getProjectState(project);
571         if (state != null) {
572             return state.getTarget();
573         }
574 
575         return null;
576     }
577 
578     /**
579      * Loads the {@link IAndroidTarget} and BuildTools for a given project.
580      * <p/>This method will get the target hash string from the project properties, and resolve
581      * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
582      * @param state the state representing the project to load.
583      * @return the target that was loaded.
584      */
585     @Nullable
loadTargetAndBuildTools(ProjectState state)586     public IAndroidTarget loadTargetAndBuildTools(ProjectState state) {
587         IAndroidTarget target = null;
588         if (state != null) {
589             String hash = state.getTargetHashString();
590             if (hash != null) {
591                 state.setTarget(target = getTargetFromHashString(hash));
592             }
593 
594             String markerMessage = null;
595             String buildToolInfoVersion = state.getBuildToolInfoVersion();
596             if (buildToolInfoVersion != null) {
597                 BuildToolInfo buildToolsInfo = getBuildToolInfo(buildToolInfoVersion);
598 
599                 if (buildToolsInfo != null) {
600                     state.setBuildToolInfo(buildToolsInfo);
601                 } else {
602                     markerMessage = String.format("Unable to resolve %s property value '%s'",
603                                         ProjectProperties.PROPERTY_BUILD_TOOLS,
604                                         buildToolInfoVersion);
605                 }
606             } else {
607                 // this is ok, we'll use the latest one automatically.
608                 state.setBuildToolInfo(null);
609             }
610 
611             handleBuildToolsMarker(state.getProject(), markerMessage);
612         }
613 
614         return target;
615     }
616 
617     /**
618      * Adds or edit a build tools marker from the given project. This is done through a Job.
619      * @param project the project
620      * @param markerMessage the message. if null the marker is removed.
621      */
handleBuildToolsMarker(final IProject project, final String markerMessage)622     private void handleBuildToolsMarker(final IProject project, final String markerMessage) {
623         Job markerJob = new Job("Android SDK: Build Tools Marker") {
624             @Override
625             protected IStatus run(IProgressMonitor monitor) {
626                 try {
627                     if (project.isAccessible()) {
628                         // always delete existing marker first
629                         project.deleteMarkers(AdtConstants.MARKER_BUILD_TOOLS, true,
630                                 IResource.DEPTH_ZERO);
631 
632                         // add the new one if needed.
633                         if (markerMessage != null) {
634                             BaseProjectHelper.markProject(project,
635                                     AdtConstants.MARKER_BUILD_TOOLS,
636                                     markerMessage, IMarker.SEVERITY_ERROR,
637                                     IMarker.PRIORITY_HIGH);
638                         }
639                     }
640                 } catch (CoreException e2) {
641                     AdtPlugin.log(e2, null);
642                     // Don't return e2.getStatus(); the job control will then produce
643                     // a popup with this error, which isn't very interesting for the
644                     // user.
645                 }
646 
647                 return Status.OK_STATUS;
648             }
649         };
650 
651         // build jobs are run after other interactive jobs
652         markerJob.setPriority(Job.BUILD);
653         markerJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
654         markerJob.schedule();
655     }
656 
657     /**
658      * Checks and loads (if needed) the data for a given target.
659      * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
660      * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
661      * <p/>An optional project as second parameter can be given to be recompiled once the target
662      * data is finished loading.
663      * <p/>The return value is non-null only if the target data has already been loaded (and in this
664      * case is the status of the load operation)
665      * @param target the target to load.
666      * @param project an optional project to be recompiled when the target data is loaded.
667      * If the target is already loaded, nothing happens.
668      * @return The load status if the target data is already loaded.
669      */
670     @NonNull
checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project)671     public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
672         boolean loadData = false;
673 
674         synchronized (LOCK) {
675             if (mDontLoadTargetData) {
676                 return LoadStatus.FAILED;
677             }
678 
679             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
680             if (bundle == null) {
681                 bundle = new TargetLoadBundle();
682                 mTargetDataStatusMap.put(target,bundle);
683 
684                 // set status to loading
685                 bundle.status = LoadStatus.LOADING;
686 
687                 // add project to bundle
688                 if (project != null) {
689                     bundle.projectsToReload.add(project);
690                 }
691 
692                 // and set the flag to start the loading below
693                 loadData = true;
694             } else if (bundle.status == LoadStatus.LOADING) {
695                 // add project to bundle
696                 if (project != null) {
697                     bundle.projectsToReload.add(project);
698                 }
699 
700                 return bundle.status;
701             } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
702                 return bundle.status;
703             }
704         }
705 
706         if (loadData) {
707             Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
708                 @Override
709                 protected IStatus run(IProgressMonitor monitor) {
710                     AdtPlugin plugin = AdtPlugin.getDefault();
711                     try {
712                         IStatus status = new AndroidTargetParser(target).run(monitor);
713 
714                         IJavaProject[] javaProjectArray = null;
715 
716                         synchronized (LOCK) {
717                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
718 
719                             if (status.getCode() != IStatus.OK) {
720                                 bundle.status = LoadStatus.FAILED;
721                                 bundle.projectsToReload.clear();
722                             } else {
723                                 bundle.status = LoadStatus.LOADED;
724 
725                                 // Prepare the array of project to recompile.
726                                 // The call is done outside of the synchronized block.
727                                 javaProjectArray = bundle.projectsToReload.toArray(
728                                         new IJavaProject[bundle.projectsToReload.size()]);
729 
730                                 // and update the UI of the editors that depend on the target data.
731                                 plugin.updateTargetListeners(target);
732                             }
733                         }
734 
735                         if (javaProjectArray != null) {
736                             ProjectHelper.updateProjects(javaProjectArray);
737                         }
738 
739                         return status;
740                     } catch (Throwable t) {
741                         synchronized (LOCK) {
742                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
743                             bundle.status = LoadStatus.FAILED;
744                         }
745 
746                         AdtPlugin.log(t, "Exception in checkAndLoadTargetData.");    //$NON-NLS-1$
747                         String message = String.format("Parsing Data for %1$s failed",  target.hashString());
748                         if (t instanceof UnsupportedClassVersionError) {
749                             message = "To use this platform, run Eclipse with JDK 7 or later. (" + message + ")";
750                         }
751                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, t);
752                     }
753                 }
754             };
755             job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
756             job.setRule(ResourcesPlugin.getWorkspace().getRoot());
757             job.schedule();
758         }
759 
760         // The only way to go through here is when the loading starts through the Job.
761         // Therefore the current status of the target is LOADING.
762         return LoadStatus.LOADING;
763     }
764 
765     /**
766      * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
767      */
768     @Nullable
getTargetData(IAndroidTarget target)769     public AndroidTargetData getTargetData(IAndroidTarget target) {
770         synchronized (LOCK) {
771             return mTargetDataMap.get(target);
772         }
773     }
774 
775     /**
776      * Return the {@link AndroidTargetData} for a given {@link IProject}.
777      */
778     @Nullable
getTargetData(IProject project)779     public AndroidTargetData getTargetData(IProject project) {
780         synchronized (LOCK) {
781             IAndroidTarget target = getTarget(project);
782             if (target != null) {
783                 return getTargetData(target);
784             }
785         }
786 
787         return null;
788     }
789 
790     /**
791      * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not
792      * loaded properly, then this will return <code>null</code>.
793      */
794     @Nullable
getDexWrapper(@ullable BuildToolInfo buildToolInfo)795     public DexWrapper getDexWrapper(@Nullable BuildToolInfo buildToolInfo) {
796         if (buildToolInfo == null) {
797             return null;
798         }
799         synchronized (LOCK) {
800             String dexLocation = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
801             DexWrapper dexWrapper = mDexWrappers.get(dexLocation);
802 
803             if (dexWrapper == null) {
804                 // load DX.
805                 dexWrapper = new DexWrapper();
806                 IStatus res = dexWrapper.loadDex(dexLocation);
807                 if (res != Status.OK_STATUS) {
808                     AdtPlugin.log(null, res.getMessage());
809                     dexWrapper = null;
810                 } else {
811                     mDexWrappers.put(dexLocation, dexWrapper);
812                 }
813             }
814 
815             return dexWrapper;
816         }
817     }
818 
unloadDexWrappers()819     public void unloadDexWrappers() {
820         synchronized (LOCK) {
821             for (DexWrapper wrapper : mDexWrappers.values()) {
822                 wrapper.unload();
823             }
824             mDexWrappers.clear();
825         }
826     }
827 
828     /**
829      * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
830      * be <code>null</code>.
831      */
832     @Nullable
getAvdManager()833     public AvdManager getAvdManager() {
834         return mAvdManager;
835     }
836 
837     @Nullable
getDeviceVersion(@onNull IDevice device)838     public static AndroidVersion getDeviceVersion(@NonNull IDevice device) {
839         try {
840             Map<String, String> props = device.getProperties();
841             String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
842             if (apiLevel == null) {
843                 return null;
844             }
845 
846             return new AndroidVersion(Integer.parseInt(apiLevel),
847                     props.get((IDevice.PROP_BUILD_CODENAME)));
848         } catch (NumberFormatException e) {
849             return null;
850         }
851     }
852 
853     @NonNull
getDeviceManager()854     public DeviceManager getDeviceManager() {
855         return mDeviceManager;
856     }
857 
858     /**
859      * Returns a list of {@link ProjectState} representing projects depending, directly or
860      * indirectly on a given library project.
861      * @param project the library project.
862      * @return a possibly empty list of ProjectState.
863      */
864     @NonNull
getMainProjectsFor(IProject project)865     public static Set<ProjectState> getMainProjectsFor(IProject project) {
866         synchronized (LOCK) {
867             // first get the project directly depending on this.
868             Set<ProjectState> list = new HashSet<ProjectState>();
869 
870             // loop on all project and see if ProjectState.getLibrary returns a non null
871             // project.
872             for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
873                 if (project != entry.getKey()) {
874                     LibraryState library = entry.getValue().getLibrary(project);
875                     if (library != null) {
876                         list.add(entry.getValue());
877                     }
878                 }
879             }
880 
881             // now look for projects depending on the projects directly depending on the library.
882             HashSet<ProjectState> result = new HashSet<ProjectState>(list);
883             for (ProjectState p : list) {
884                 if (p.isLibrary()) {
885                     Set<ProjectState> set = getMainProjectsFor(p.getProject());
886                     result.addAll(set);
887                 }
888             }
889 
890             return result;
891         }
892     }
893 
894     /**
895      * Unload the SDK's target data.
896      *
897      * If <var>preventReload</var>, this effect is final until the SDK instance is changed
898      * through {@link #loadSdk(String)}.
899      *
900      * The goal is to unload the targets to be able to replace existing targets with new ones,
901      * before calling {@link #loadSdk(String)} to fully reload the SDK.
902      *
903      * @param preventReload prevent the data from being loaded again for the remaining live of
904      *   this {@link Sdk} instance.
905      */
unloadTargetData(boolean preventReload)906     public void unloadTargetData(boolean preventReload) {
907         synchronized (LOCK) {
908             mDontLoadTargetData = preventReload;
909 
910             // dispose of the target data.
911             for (AndroidTargetData data : mTargetDataMap.values()) {
912                 data.dispose();
913             }
914 
915             mTargetDataMap.clear();
916         }
917     }
918 
Sdk(SdkManager manager, AvdManager avdManager)919     private Sdk(SdkManager manager, AvdManager avdManager) {
920         mManager = manager;
921         mAvdManager = avdManager;
922 
923         // listen to projects closing
924         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
925         // need to register the resource event listener first because the project listener
926         // is called back during registration with project opened in the workspace.
927         monitor.addResourceEventListener(mResourceEventListener);
928         monitor.addProjectListener(mProjectListener);
929         monitor.addFileListener(mFileListener,
930                 IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED);
931 
932         // pre-compute some paths
933         mDocBaseUrl = getDocumentationBaseUrl(manager.getLocation() +
934                 SdkConstants.OS_SDK_DOCS_FOLDER);
935 
936         mDeviceManager = DeviceManager.createInstance(manager.getLocalSdk().getLocation(),
937                                                       AdtPlugin.getDefault());
938 
939         // update whatever ProjectState is already present with new IAndroidTarget objects.
940         synchronized (LOCK) {
941             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
942                 loadTargetAndBuildTools(entry.getValue());
943             }
944         }
945     }
946 
947     /**
948      *  Cleans and unloads the SDK.
949      */
dispose()950     private void dispose() {
951         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
952         monitor.removeProjectListener(mProjectListener);
953         monitor.removeFileListener(mFileListener);
954         monitor.removeResourceEventListener(mResourceEventListener);
955 
956         // the IAndroidTarget objects are now obsolete so update the project states.
957         synchronized (LOCK) {
958             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
959                 entry.getValue().setTarget(null);
960             }
961 
962             // dispose of the target data.
963             for (AndroidTargetData data : mTargetDataMap.values()) {
964                 data.dispose();
965             }
966 
967             mTargetDataMap.clear();
968         }
969     }
970 
setTargetData(IAndroidTarget target, AndroidTargetData data)971     void setTargetData(IAndroidTarget target, AndroidTargetData data) {
972         synchronized (LOCK) {
973             mTargetDataMap.put(target, data);
974         }
975     }
976 
977     /**
978      * Returns the URL to the local documentation.
979      * Can return null if no documentation is found in the current SDK.
980      *
981      * @param osDocsPath Path to the documentation folder in the current SDK.
982      *  The folder may not actually exist.
983      * @return A file:// URL on the local documentation folder if it exists or null.
984      */
getDocumentationBaseUrl(String osDocsPath)985     private String getDocumentationBaseUrl(String osDocsPath) {
986         File f = new File(osDocsPath);
987 
988         if (f.isDirectory()) {
989             try {
990                 // Note: to create a file:// URL, one would typically use something like
991                 // f.toURI().toURL().toString(). However this generates a broken path on
992                 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
993                 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
994                 // do the correct thing manually.
995 
996                 String path = f.getAbsolutePath();
997                 if (File.separatorChar != '/') {
998                     path = path.replace(File.separatorChar, '/');
999                 }
1000 
1001                 // For some reason the URL class doesn't add the mandatory "//" after
1002                 // the "file:" protocol name, so it has to be hacked into the path.
1003                 URL url = new URL("file", null, "//" + path);  //$NON-NLS-1$ //$NON-NLS-2$
1004                 String result = url.toString();
1005                 return result;
1006             } catch (MalformedURLException e) {
1007                 // ignore malformed URLs
1008             }
1009         }
1010 
1011         return null;
1012     }
1013 
1014     /**
1015      * Delegate listener for project changes.
1016      */
1017     private IProjectListener mProjectListener = new IProjectListener() {
1018         @Override
1019         public void projectClosed(IProject project) {
1020             onProjectRemoved(project, false /*deleted*/);
1021         }
1022 
1023         @Override
1024         public void projectDeleted(IProject project) {
1025             onProjectRemoved(project, true /*deleted*/);
1026         }
1027 
1028         private void onProjectRemoved(IProject removedProject, boolean deleted) {
1029             if (DEBUG) {
1030                 System.out.println(">>> CLOSED: " + removedProject.getName());
1031             }
1032 
1033             // get the target project
1034             synchronized (LOCK) {
1035                 // Don't use getProject() as it could create the ProjectState if it's not
1036                 // there yet and this is not what we want. We want the current object.
1037                 // Therefore, direct access to the map.
1038                 ProjectState removedState = sProjectStateMap.get(removedProject);
1039                 if (removedState != null) {
1040                     // 1. clear the layout lib cache associated with this project
1041                     IAndroidTarget target = removedState.getTarget();
1042                     if (target != null) {
1043                         // get the bridge for the target, and clear the cache for this project.
1044                         AndroidTargetData data = mTargetDataMap.get(target);
1045                         if (data != null) {
1046                             LayoutLibrary layoutLib = data.getLayoutLibrary();
1047                             if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) {
1048                                 layoutLib.clearCaches(removedProject);
1049                             }
1050                         }
1051                     }
1052 
1053                     // 2. if the project is a library, make sure to update the
1054                     // LibraryState for any project referencing it.
1055                     // Also, record the updated projects that are libraries, to update
1056                     // projects that depend on them.
1057                     for (ProjectState projectState : sProjectStateMap.values()) {
1058                         LibraryState libState = projectState.getLibrary(removedProject);
1059                         if (libState != null) {
1060                             // Close the library right away.
1061                             // This remove links between the LibraryState and the projectState.
1062                             // This is because in case of a rename of a project, projectClosed and
1063                             // projectOpened will be called before any other job is run, so we
1064                             // need to make sure projectOpened is closed with the main project
1065                             // state up to date.
1066                             libState.close();
1067 
1068                             // record that this project changed, and in case it's a library
1069                             // that its parents need to be updated as well.
1070                             markProject(projectState, projectState.isLibrary());
1071                         }
1072                     }
1073 
1074                     // now remove the project for the project map.
1075                     sProjectStateMap.remove(removedProject);
1076                 }
1077             }
1078 
1079             if (DEBUG) {
1080                 System.out.println("<<<");
1081             }
1082         }
1083 
1084         @Override
1085         public void projectOpened(IProject project) {
1086             onProjectOpened(project);
1087         }
1088 
1089         @Override
1090         public void projectOpenedWithWorkspace(IProject project) {
1091             // no need to force recompilation when projects are opened with the workspace.
1092             onProjectOpened(project);
1093         }
1094 
1095         @Override
1096         public void allProjectsOpenedWithWorkspace() {
1097             // Correct currently open editors
1098             fixOpenLegacyEditors();
1099         }
1100 
1101         private void onProjectOpened(final IProject openedProject) {
1102 
1103             ProjectState openedState = getProjectState(openedProject);
1104             if (openedState != null) {
1105                 if (DEBUG) {
1106                     System.out.println(">>> OPENED: " + openedProject.getName());
1107                 }
1108 
1109                 synchronized (LOCK) {
1110                     final boolean isLibrary = openedState.isLibrary();
1111                     final boolean hasLibraries = openedState.hasLibraries();
1112 
1113                     if (isLibrary || hasLibraries) {
1114                         boolean foundLibraries = false;
1115                         // loop on all the existing project and update them based on this new
1116                         // project
1117                         for (ProjectState projectState : sProjectStateMap.values()) {
1118                             if (projectState != openedState) {
1119                                 // If the project has libraries, check if this project
1120                                 // is a reference.
1121                                 if (hasLibraries) {
1122                                     // ProjectState#needs() both checks if this is a missing library
1123                                     // and updates LibraryState to contains the new values.
1124                                     // This must always be called.
1125                                     LibraryState libState = openedState.needs(projectState);
1126 
1127                                     if (libState != null) {
1128                                         // found a library! Add the main project to the list of
1129                                         // modified project
1130                                         foundLibraries = true;
1131                                     }
1132                                 }
1133 
1134                                 // if the project is a library check if the other project depend
1135                                 // on it.
1136                                 if (isLibrary) {
1137                                     // ProjectState#needs() both checks if this is a missing library
1138                                     // and updates LibraryState to contains the new values.
1139                                     // This must always be called.
1140                                     LibraryState libState = projectState.needs(openedState);
1141 
1142                                     if (libState != null) {
1143                                         // There's a dependency! Add the project to the list of
1144                                         // modified project, but also to a list of projects
1145                                         // that saw one of its dependencies resolved.
1146                                         markProject(projectState, projectState.isLibrary());
1147                                     }
1148                                 }
1149                             }
1150                         }
1151 
1152                         // if the project has a libraries and we found at least one, we add
1153                         // the project to the list of modified project.
1154                         // Since we already went through the parent, no need to update them.
1155                         if (foundLibraries) {
1156                             markProject(openedState, false /*updateParents*/);
1157                         }
1158                     }
1159                 }
1160 
1161                 // Correct file editor associations.
1162                 fixEditorAssociations(openedProject);
1163 
1164                 // Fix classpath entries in a job since the workspace might be locked now.
1165                 Job fixCpeJob = new Job("Adjusting Android Project Classpath") {
1166                     @Override
1167                     protected IStatus run(IProgressMonitor monitor) {
1168                         try {
1169                             ProjectHelper.fixProjectClasspathEntries(
1170                                     JavaCore.create(openedProject));
1171                         } catch (JavaModelException e) {
1172                             AdtPlugin.log(e, "error fixing classpath entries");
1173                             // Don't return e2.getStatus(); the job control will then produce
1174                             // a popup with this error, which isn't very interesting for the
1175                             // user.
1176                         }
1177 
1178                         return Status.OK_STATUS;
1179                     }
1180                 };
1181 
1182                 // build jobs are run after other interactive jobs
1183                 fixCpeJob.setPriority(Job.BUILD);
1184                 fixCpeJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
1185                 fixCpeJob.schedule();
1186 
1187 
1188                 if (DEBUG) {
1189                     System.out.println("<<<");
1190                 }
1191             }
1192         }
1193 
1194         @Override
1195         public void projectRenamed(IProject project, IPath from) {
1196             // we don't actually care about this anymore.
1197         }
1198     };
1199 
1200     /**
1201      * Delegate listener for file changes.
1202      */
1203     private IFileListener mFileListener = new IFileListener() {
1204         @Override
1205         public void fileChanged(final @NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
1206                 int kind, @Nullable String extension, int flags, boolean isAndroidPRoject) {
1207             if (!isAndroidPRoject) {
1208                 return;
1209             }
1210 
1211             if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) &&
1212                     file.getParent() == file.getProject()) {
1213                 try {
1214                     // reload the content of the project.properties file and update
1215                     // the target.
1216                     IProject iProject = file.getProject();
1217 
1218                     ProjectState state = Sdk.getProjectState(iProject);
1219 
1220                     // get the current target and build tools
1221                     IAndroidTarget oldTarget = state.getTarget();
1222                     boolean oldRsSupportMode = state.getRenderScriptSupportMode();
1223 
1224                     // get the current library flag
1225                     boolean wasLibrary = state.isLibrary();
1226 
1227                     LibraryDifference diff = state.reloadProperties();
1228 
1229                     // load the (possibly new) target.
1230                     IAndroidTarget newTarget = loadTargetAndBuildTools(state);
1231 
1232                     // reload the libraries if needed
1233                     if (diff.hasDiff()) {
1234                         if (diff.added) {
1235                             synchronized (LOCK) {
1236                                 for (ProjectState projectState : sProjectStateMap.values()) {
1237                                     if (projectState != state) {
1238                                         // need to call needs to do the libraryState link,
1239                                         // but no need to look at the result, as we'll compare
1240                                         // the result of getFullLibraryProjects()
1241                                         // this is easier to due to indirect dependencies.
1242                                         state.needs(projectState);
1243                                     }
1244                                 }
1245                             }
1246                         }
1247 
1248                         markProject(state, wasLibrary || state.isLibrary());
1249                     }
1250 
1251                     // apply the new target if needed.
1252                     if (newTarget != oldTarget ||
1253                             oldRsSupportMode != state.getRenderScriptSupportMode()) {
1254                         IJavaProject javaProject = BaseProjectHelper.getJavaProject(
1255                                 file.getProject());
1256                         if (javaProject != null) {
1257                             ProjectHelper.updateProject(javaProject);
1258                         }
1259 
1260                         // update the editors to reload with the new target
1261                         AdtPlugin.getDefault().updateTargetListeners(iProject);
1262                     }
1263                 } catch (CoreException e) {
1264                     // This can't happen as it's only for closed project (or non existing)
1265                     // but in that case we can't get a fileChanged on this file.
1266                 }
1267             } else if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) {
1268                 // check if it's an add/remove on a jar files inside libs
1269                 if (EXT_JAR.equals(extension) &&
1270                         file.getProjectRelativePath().segmentCount() == 2 &&
1271                         file.getParent().getName().equals(SdkConstants.FD_NATIVE_LIBS)) {
1272                     // need to update the project and whatever depend on it.
1273 
1274                     processJarFileChange(file);
1275                 }
1276             }
1277         }
1278 
1279         private void processJarFileChange(final IFile file) {
1280             try {
1281                 IProject iProject = file.getProject();
1282 
1283                 if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
1284                     return;
1285                 }
1286 
1287                 List<IJavaProject> projectList = new ArrayList<IJavaProject>();
1288                 IJavaProject javaProject = BaseProjectHelper.getJavaProject(iProject);
1289                 if (javaProject != null) {
1290                     projectList.add(javaProject);
1291                 }
1292 
1293                 ProjectState state = Sdk.getProjectState(iProject);
1294 
1295                 if (state != null) {
1296                     Collection<ProjectState> parents = state.getFullParentProjects();
1297                     for (ProjectState s : parents) {
1298                         javaProject = BaseProjectHelper.getJavaProject(s.getProject());
1299                         if (javaProject != null) {
1300                             projectList.add(javaProject);
1301                         }
1302                     }
1303 
1304                     ProjectHelper.updateProjects(
1305                             projectList.toArray(new IJavaProject[projectList.size()]));
1306                 }
1307             } catch (CoreException e) {
1308                 // This can't happen as it's only for closed project (or non existing)
1309                 // but in that case we can't get a fileChanged on this file.
1310             }
1311         }
1312     };
1313 
1314     /** List of modified projects. This is filled in
1315      * {@link IProjectListener#projectOpened(IProject)},
1316      * {@link IProjectListener#projectOpenedWithWorkspace(IProject)},
1317      * {@link IProjectListener#projectClosed(IProject)}, and
1318      * {@link IProjectListener#projectDeleted(IProject)} and processed in
1319      * {@link IResourceEventListener#resourceChangeEventEnd()}.
1320      */
1321     private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>();
1322     private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>();
1323 
markProject(ProjectState projectState, boolean updateParents)1324     private void markProject(ProjectState projectState, boolean updateParents) {
1325         if (mModifiedProjects.contains(projectState) == false) {
1326             if (DEBUG) {
1327                 System.out.println("\tMARKED: " + projectState.getProject().getName());
1328             }
1329             mModifiedProjects.add(projectState);
1330         }
1331 
1332         // if the project is resolved also add it to this list.
1333         if (updateParents) {
1334             if (mModifiedChildProjects.contains(projectState) == false) {
1335                 if (DEBUG) {
1336                     System.out.println("\tMARKED(child): " + projectState.getProject().getName());
1337                 }
1338                 mModifiedChildProjects.add(projectState);
1339             }
1340         }
1341     }
1342 
1343     /**
1344      * Delegate listener for resource changes. This is called before and after any calls to the
1345      * project and file listeners (for a given resource change event).
1346      */
1347     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
1348         @Override
1349         public void resourceChangeEventStart() {
1350             mModifiedProjects.clear();
1351             mModifiedChildProjects.clear();
1352         }
1353 
1354         @Override
1355         public void resourceChangeEventEnd() {
1356             if (mModifiedProjects.size() == 0) {
1357                 return;
1358             }
1359 
1360             // first make sure all the parents are updated
1361             updateParentProjects();
1362 
1363             // for all modified projects, update their library list
1364             // and gather their IProject
1365             final List<IJavaProject> projectList = new ArrayList<IJavaProject>();
1366             for (ProjectState state : mModifiedProjects) {
1367                 state.updateFullLibraryList();
1368                 projectList.add(JavaCore.create(state.getProject()));
1369             }
1370 
1371             Job job = new Job("Android Library Update") { //$NON-NLS-1$
1372                 @Override
1373                 protected IStatus run(IProgressMonitor monitor) {
1374                     LibraryClasspathContainerInitializer.updateProjects(
1375                             projectList.toArray(new IJavaProject[projectList.size()]));
1376 
1377                     for (IJavaProject javaProject : projectList) {
1378                         try {
1379                             javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD,
1380                                     monitor);
1381                         } catch (CoreException e) {
1382                             // pass
1383                         }
1384                     }
1385                     return Status.OK_STATUS;
1386                 }
1387             };
1388             job.setPriority(Job.BUILD);
1389             job.setRule(ResourcesPlugin.getWorkspace().getRoot());
1390             job.schedule();
1391         }
1392     };
1393 
1394     /**
1395      * Updates all existing projects with a given list of new/updated libraries.
1396      * This loops through all opened projects and check if they depend on any of the given
1397      * library project, and if they do, they are linked together.
1398      */
updateParentProjects()1399     private void updateParentProjects() {
1400         if (mModifiedChildProjects.size() == 0) {
1401             return;
1402         }
1403 
1404         ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects);
1405         mModifiedChildProjects.clear();
1406         synchronized (LOCK) {
1407             // for each project for which we must update its parent, we loop on the parent
1408             // projects and adds them to the list of modified projects. If they are themselves
1409             // libraries, we add them too.
1410             for (ProjectState state : childProjects) {
1411                 if (DEBUG) {
1412                     System.out.println(">>> Updating parents of " + state.getProject().getName());
1413                 }
1414                 List<ProjectState> parents = state.getParentProjects();
1415                 for (ProjectState parent : parents) {
1416                     markProject(parent, parent.isLibrary());
1417                 }
1418                 if (DEBUG) {
1419                     System.out.println("<<<");
1420                 }
1421             }
1422         }
1423 
1424         // done, but there may be parents that are also libraries. Need to update their parents.
1425         updateParentProjects();
1426     }
1427 
1428     /**
1429      * Fix editor associations for the given project, if not already done.
1430      * <p/>
1431      * Eclipse has a per-file setting for which editor should be used for each file
1432      * (see {@link IDE#setDefaultEditor(IFile, String)}).
1433      * We're using this flag to pick between the various XML editors (layout, drawable, etc)
1434      * since they all have the same file name extension.
1435      * <p/>
1436      * Unfortunately, the file setting can be "wrong" for two reasons:
1437      * <ol>
1438      *   <li> The editor type was added <b>after</b> a file had been seen by the IDE.
1439      *        For example, we added new editors for animations and for drawables around
1440      *        ADT 12, but any file seen by ADT in earlier versions will continue to use
1441      *        the vanilla Eclipse XML editor instead.
1442      *   <li> A bug in ADT 14 and ADT 15 (see issue 21124) meant that files created in new
1443      *        folders would end up with wrong editor associations. Even though that bug
1444      *        is fixed in ADT 16, the fix only affects new files, it cannot retroactively
1445      *        fix editor associations that were set incorrectly by ADT 14 or 15.
1446      * </ol>
1447      * <p/>
1448      * This method attempts to fix the editor bindings retroactively by scanning all the
1449      * resource XML files and resetting the editor associations.
1450      * Since this is a potentially slow operation, this is only done "once"; we use a
1451      * persistent project property to avoid looking repeatedly. In the future if we add
1452      * additional editors, we can rev the scanned version value.
1453      */
fixEditorAssociations(final IProject project)1454     private void fixEditorAssociations(final IProject project) {
1455         QualifiedName KEY = new QualifiedName(AdtPlugin.PLUGIN_ID, "editorbinding"); //$NON-NLS-1$
1456 
1457         try {
1458             String value = project.getPersistentProperty(KEY);
1459             int currentVersion = 0;
1460             if (value != null) {
1461                 try {
1462                     currentVersion = Integer.parseInt(value);
1463                 } catch (Exception ingore) {
1464                 }
1465             }
1466 
1467             // The target version we're comparing to. This must be incremented each time
1468             // we change the processing here so that a new version of the plugin would
1469             // try to fix existing user projects.
1470             final int targetVersion = 2;
1471 
1472             if (currentVersion >= targetVersion) {
1473                 return;
1474             }
1475 
1476             // Set to specific version such that we can rev the version in the future
1477             // to trigger further scanning
1478             project.setPersistentProperty(KEY, Integer.toString(targetVersion));
1479 
1480             // Now update the actual editor associations.
1481             Job job = new Job("Update Android editor bindings") { //$NON-NLS-1$
1482                 @Override
1483                 protected IStatus run(IProgressMonitor monitor) {
1484                     try {
1485                         for (IResource folderResource : project.getFolder(FD_RES).members()) {
1486                             if (folderResource instanceof IFolder) {
1487                                 IFolder folder = (IFolder) folderResource;
1488 
1489                                 for (IResource resource : folder.members()) {
1490                                     if (resource instanceof IFile &&
1491                                             resource.getName().endsWith(DOT_XML)) {
1492                                         fixXmlFile((IFile) resource);
1493                                     }
1494                                 }
1495                             }
1496                         }
1497 
1498                         // TODO change AndroidManifest.xml ID too
1499 
1500                     } catch (CoreException e) {
1501                         AdtPlugin.log(e, null);
1502                     }
1503 
1504                     return Status.OK_STATUS;
1505                 }
1506 
1507                 /**
1508                  * Attempt to fix the editor ID for the given /res XML file.
1509                  */
1510                 private void fixXmlFile(final IFile file) {
1511                     // Fix the default editor ID for this resource.
1512                     // This has no effect on currently open editors.
1513                     IEditorDescriptor desc = IDE.getDefaultEditor(file);
1514 
1515                     if (desc == null || !CommonXmlEditor.ID.equals(desc.getId())) {
1516                         IDE.setDefaultEditor(file, CommonXmlEditor.ID);
1517                     }
1518                 }
1519             };
1520             job.setPriority(Job.BUILD);
1521             job.schedule();
1522         } catch (CoreException e) {
1523             AdtPlugin.log(e, null);
1524         }
1525     }
1526 
1527     /**
1528      * Tries to fix all currently open Android legacy editors.
1529      * <p/>
1530      * If an editor is found to match one of the legacy ids, we'll try to close it.
1531      * If that succeeds, we try to reopen it using the new common editor ID.
1532      * <p/>
1533      * This method must be run from the UI thread.
1534      */
fixOpenLegacyEditors()1535     private void fixOpenLegacyEditors() {
1536 
1537         AdtPlugin adt = AdtPlugin.getDefault();
1538         if (adt == null) {
1539             return;
1540         }
1541 
1542         final IPreferenceStore store = adt.getPreferenceStore();
1543         int currentValue = store.getInt(AdtPrefs.PREFS_FIX_LEGACY_EDITORS);
1544         // The target version we're comparing to. This must be incremented each time
1545         // we change the processing here so that a new version of the plugin would
1546         // try to fix existing editors.
1547         final int targetValue = 1;
1548 
1549         if (currentValue >= targetValue) {
1550             return;
1551         }
1552 
1553         // To be able to close and open editors we need to make sure this is done
1554         // in the UI thread, which this isn't invoked from.
1555         PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
1556             @Override
1557             public void run() {
1558                 HashSet<String> legacyIds =
1559                     new HashSet<String>(Arrays.asList(CommonXmlEditor.LEGACY_EDITOR_IDS));
1560 
1561                 for (IWorkbenchWindow win : PlatformUI.getWorkbench().getWorkbenchWindows()) {
1562                     for (IWorkbenchPage page : win.getPages()) {
1563                         for (IEditorReference ref : page.getEditorReferences()) {
1564                             try {
1565                                 IEditorInput input = ref.getEditorInput();
1566                                 if (input instanceof IFileEditorInput) {
1567                                     IFile file = ((IFileEditorInput)input).getFile();
1568                                     IEditorPart part = ref.getEditor(true /*restore*/);
1569                                     if (part != null) {
1570                                         IWorkbenchPartSite site = part.getSite();
1571                                         if (site != null) {
1572                                             String id = site.getId();
1573                                             if (legacyIds.contains(id)) {
1574                                                 // This editor matches one of legacy editor IDs.
1575                                                 fixEditor(page, part, input, file, id);
1576                                             }
1577                                         }
1578                                     }
1579                                 }
1580                             } catch (Exception e) {
1581                                 // ignore
1582                             }
1583                         }
1584                     }
1585                 }
1586 
1587                 // Remember that we managed to do fix all editors
1588                 store.setValue(AdtPrefs.PREFS_FIX_LEGACY_EDITORS, targetValue);
1589             }
1590 
1591             private void fixEditor(
1592                     IWorkbenchPage page,
1593                     IEditorPart part,
1594                     IEditorInput input,
1595                     IFile file,
1596                     String id) {
1597                 IDE.setDefaultEditor(file, CommonXmlEditor.ID);
1598 
1599                 boolean ok = page.closeEditor(part, true /*save*/);
1600 
1601                 AdtPlugin.log(IStatus.INFO,
1602                     "Closed legacy editor ID %s for %s: %s", //$NON-NLS-1$
1603                     id,
1604                     file.getFullPath(),
1605                     ok ? "Success" : "Failed");//$NON-NLS-1$ //$NON-NLS-2$
1606 
1607                 if (ok) {
1608                     // Try to reopen it with the new ID
1609                     try {
1610                         page.openEditor(input, CommonXmlEditor.ID);
1611                     } catch (PartInitException e) {
1612                         AdtPlugin.log(e,
1613                             "Failed to reopen %s",          //$NON-NLS-1$
1614                             file.getFullPath());
1615                     }
1616                 }
1617             }
1618         });
1619     }
1620 }
1621