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