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