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