com.android.ide.eclipse.adt.internal.sdk.Sdk.java Source code

Java tutorial

Introduction

Here is the source code for com.android.ide.eclipse.adt.internal.sdk.Sdk.java

Source

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.sdk;

import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.EXT_JAR;
import static com.android.SdkConstants.FD_RES;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ddmlib.IDevice;
import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.build.DexWrapper;
import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
import com.android.io.StreamException;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
import com.android.sdklib.devices.DeviceManager;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
import com.android.sdklib.repository.FullRevision;
import com.android.utils.ILogger;
import com.google.common.collect.Maps;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
 * at the same time.
 *
 * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
 * the Sdk object.
 *
 * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
 */
public final class Sdk {
    private final static boolean DEBUG = false;

    private final static Object LOCK = new Object();

    private static Sdk sCurrentSdk = null;

    /**
     * Map associating {@link IProject} and their state {@link ProjectState}.
     * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
     */
    private final static HashMap<IProject, ProjectState> sProjectStateMap = new HashMap<IProject, ProjectState>();

    /**
     * Data bundled using during the load of Target data.
     * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
     * to compile before the loading was finished. Those projects will be recompiled
     * at the end of the loading.
     */
    private final static class TargetLoadBundle {
        LoadStatus status;
        final HashSet<IJavaProject> projectsToReload = new HashSet<IJavaProject>();
    }

    private final SdkManager mManager;
    private final Map<String, DexWrapper> mDexWrappers = Maps.newHashMap();
    private final AvdManager mAvdManager;
    private final DeviceManager mDeviceManager;

    /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
    private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = new HashMap<IAndroidTarget, AndroidTargetData>();
    /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
    private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap = new HashMap<IAndroidTarget, TargetLoadBundle>();

    /**
     * If true the target data will never load anymore. The only way to reload them is to
     * completely reload the SDK with {@link #loadSdk(String)}
     */
    private boolean mDontLoadTargetData = false;

    private final String mDocBaseUrl;

    /**
     * Classes implementing this interface will receive notification when targets are changed.
     */
    public interface ITargetChangeListener {
        /**
         * Sent when project has its target changed.
         */
        void onProjectTargetChange(IProject changedProject);

        /**
         * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
         * or the SDK is changed).
         */
        void onTargetLoaded(IAndroidTarget target);

        /**
         * Called when the base content of the SDK is parsed.
         */
        void onSdkLoaded();
    }

    /**
     * Basic abstract implementation of the ITargetChangeListener for the case where both
     * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
     * use the same code based on a simple test requiring to know the current IProject.
     */
    public static abstract class TargetChangeListener implements ITargetChangeListener {
        /**
         * Returns the {@link IProject} associated with the listener.
         */
        public abstract IProject getProject();

        /**
         * Called when the listener needs to take action on the event. This is only called
         * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
         * match the values received in {@link #onProjectTargetChange(IProject)} and
         * {@link #onTargetLoaded(IAndroidTarget)}.
         */
        public abstract void reload();

        @Override
        public void onProjectTargetChange(IProject changedProject) {
            if (changedProject != null && changedProject.equals(getProject())) {
                reload();
            }
        }

        @Override
        public void onTargetLoaded(IAndroidTarget target) {
            IProject project = getProject();
            if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
                reload();
            }
        }

        @Override
        public void onSdkLoaded() {
            // do nothing;
        }
    }

    /**
     * Returns the lock object used to synchronize all operations dealing with SDK, targets and
     * projects.
     */
    @NonNull
    public static final Object getLock() {
        return LOCK;
    }

    /**
     * Loads an SDK and returns an {@link Sdk} object if success.
     * <p/>If the SDK failed to load, it displays an error to the user.
     * @param sdkLocation the OS path to the SDK.
     */
    @Nullable
    public static Sdk loadSdk(String sdkLocation) {
        synchronized (LOCK) {
            if (sCurrentSdk != null) {
                sCurrentSdk.dispose();
                sCurrentSdk = null;
            }

            final AtomicBoolean hasWarning = new AtomicBoolean();
            final AtomicBoolean hasError = new AtomicBoolean();
            final ArrayList<String> logMessages = new ArrayList<String>();
            ILogger log = new ILogger() {
                @Override
                public void error(@Nullable Throwable throwable, @Nullable String errorFormat, Object... arg) {
                    hasError.set(true);
                    if (errorFormat != null) {
                        logMessages.add(String.format("Error: " + errorFormat, arg));
                    }

                    if (throwable != null) {
                        logMessages.add(throwable.getMessage());
                    }
                }

                @Override
                public void warning(@NonNull String warningFormat, Object... arg) {
                    hasWarning.set(true);
                    logMessages.add(String.format("Warning: " + warningFormat, arg));
                }

                @Override
                public void info(@NonNull String msgFormat, Object... arg) {
                    logMessages.add(String.format(msgFormat, arg));
                }

                @Override
                public void verbose(@NonNull String msgFormat, Object... arg) {
                    info(msgFormat, arg);
                }
            };

            // get an SdkManager object for the location
            SdkManager manager = SdkManager.createManager(sdkLocation, log);
            try {
                if (manager == null) {
                    hasError.set(true);
                } else {
                    // create the AVD Manager
                    AvdManager avdManager = null;
                    try {
                        avdManager = AvdManager.getInstance(manager.getLocalSdk(), log);
                    } catch (AndroidLocationException e) {
                        log.error(e, "Error parsing the AVDs");
                    }
                    sCurrentSdk = new Sdk(manager, avdManager);
                    return sCurrentSdk;
                }
            } finally {
                if (hasError.get() || hasWarning.get()) {
                    StringBuilder sb = new StringBuilder(
                            String.format("%s when loading the SDK:\n", hasError.get() ? "Error" : "Warning"));
                    for (String msg : logMessages) {
                        sb.append('\n');
                        sb.append(msg);
                    }
                    if (hasError.get()) {
                        AdtPlugin.printErrorToConsole("Android SDK", sb.toString());
                        AdtPlugin.displayError("Android SDK", sb.toString());
                    } else {
                        AdtPlugin.printToConsole("Android SDK", sb.toString());
                    }
                }
            }
            return null;
        }
    }

    /**
     * Returns the current {@link Sdk} object.
     */
    @Nullable
    public static Sdk getCurrent() {
        synchronized (LOCK) {
            return sCurrentSdk;
        }
    }

    /**
     * Returns the location of the current SDK as an OS path string.
     * Guaranteed to be terminated by a platform-specific path separator.
     * <p/>
     * Due to {@link File} canonicalization, this MAY differ from the string used to initialize
     * the SDK path.
     *
     * @return The SDK OS path or null if no SDK is setup.
     * @deprecated Consider using {@link #getSdkFileLocation()} instead.
     * @see #getSdkFileLocation()
     */
    @Deprecated
    @Nullable
    public String getSdkOsLocation() {
        String path = mManager == null ? null : mManager.getLocation();
        if (path != null) {
            // For backward compatibility make sure it ends with a separator.
            // This used to be the case when the SDK Manager was created from a String path
            // but now that a File is internally used the trailing dir separator is lost.
            if (path.length() > 0 && !path.endsWith(File.separator)) {
                path = path + File.separator;
            }
        }
        return path;
    }

    /**
     * Returns the location of the current SDK as a {@link File} or null.
     *
     * @return The SDK OS path or null if no SDK is setup.
     */
    @Nullable
    public File getSdkFileLocation() {
        if (mManager == null || mManager.getLocalSdk() == null) {
            return null;
        }
        return mManager.getLocalSdk().getLocation();
    }

    /**
     * Returns a <em>new</em> {@link SdkManager} that can parse the SDK located
     * at the current {@link #getSdkOsLocation()}.
     * <p/>
     * Implementation detail: The {@link Sdk} has its own internal manager with
     * a custom logger which is not designed to be useful for outsiders. Callers
     * who need their own {@link SdkManager} for parsing will often want to control
     * the logger for their own need.
     * <p/>
     * This is just a convenient method equivalent to writing:
     * <pre>SdkManager.createManager(Sdk.getCurrent().getSdkLocation(), log);</pre>
     *
     * @param log The logger for the {@link SdkManager}.
     * @return A new {@link SdkManager} parsing the same location.
     */
    public @Nullable SdkManager getNewSdkManager(@NonNull ILogger log) {
        return SdkManager.createManager(getSdkOsLocation(), log);
    }

    /**
     * Returns the URL to the local documentation.
     * Can return null if no documentation is found in the current SDK.
     *
     * @return A file:// URL on the local documentation folder if it exists or null.
     */
    @Nullable
    public String getDocumentationBaseUrl() {
        return mDocBaseUrl;
    }

    /**
     * Returns the list of targets that are available in the SDK.
     */
    public IAndroidTarget[] getTargets() {
        return mManager.getTargets();
    }

    /**
     * Queries the underlying SDK Manager to check whether the platforms or addons
     * directories have changed on-disk. Does not reload the SDK.
     * <p/>
     * This is a quick test based on the presence of the directories, their timestamps
     * and a quick checksum of the source.properties files. It's possible to have
     * false positives (e.g. if a file is manually modified in a platform) or false
     * negatives (e.g. if a platform data file is changed manually in a 2nd level
     * directory without altering the source.properties.)
     */
    public boolean haveTargetsChanged() {
        return mManager.hasChanged();
    }

    /**
     * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
     *
     * @param hash the {@link IAndroidTarget} hash string.
     * @return The matching {@link IAndroidTarget} or null.
     */
    @Nullable
    public IAndroidTarget getTargetFromHashString(@NonNull String hash) {
        return mManager.getTargetFromHashString(hash);
    }

    @Nullable
    public BuildToolInfo getBuildToolInfo(@Nullable String buildToolVersion) {
        if (buildToolVersion != null) {
            try {
                return mManager.getBuildTool(FullRevision.parseRevision(buildToolVersion));
            } catch (Exception e) {
                // ignore, return null below.
            }
        }

        return null;
    }

    @Nullable
    public BuildToolInfo getLatestBuildTool() {
        return mManager.getLatestBuildTool();
    }

    /**
     * Initializes a new project with a target. This creates the <code>project.properties</code>
     * file.
     * @param project the project to initialize
     * @param target the project's target.
     * @throws IOException if creating the file failed in any way.
     * @throws StreamException if processing the project property file fails
     */
    public void initProject(@Nullable IProject project, @Nullable IAndroidTarget target)
            throws IOException, StreamException {
        if (project == null || target == null) {
            return;
        }

        synchronized (LOCK) {
            // check if there's already a state?
            ProjectState state = getProjectState(project);

            ProjectPropertiesWorkingCopy properties = null;

            if (state != null) {
                properties = state.getProperties().makeWorkingCopy();
            }

            if (properties == null) {
                IPath location = project.getLocation();
                if (location == null) { // can return null when the project is being deleted.
                    // do nothing and return null;
                    return;
                }

                properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT);
            }

            // save the target hash string in the project persistent property
            properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
            properties.save();
        }
    }

    /**
     * Returns the {@link ProjectState} object associated with a given project.
     * <p/>
     * This method is the only way to properly get the project's {@link ProjectState}
     * If the project has not yet been loaded, then it is loaded.
     * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
     * objects, and therefore is static.
     * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
     * are replaced.
     * @param project the request project
     * @return the ProjectState for the project.
     */
    @Nullable
    @SuppressWarnings("deprecation")
    public static ProjectState getProjectState(IProject project) {
        if (project == null) {
            return null;
        }

        synchronized (LOCK) {
            ProjectState state = sProjectStateMap.get(project);
            if (state == null) {
                // load the project.properties from the project folder.
                IPath location = project.getLocation();
                if (location == null) { // can return null when the project is being deleted.
                    // do nothing and return null;
                    return null;
                }

                String projectLocation = location.toOSString();

                ProjectProperties properties = ProjectProperties.load(projectLocation, PropertyType.PROJECT);
                if (properties == null) {
                    // legacy support: look for default.properties and rename it if needed.
                    properties = ProjectProperties.load(projectLocation, PropertyType.LEGACY_DEFAULT);

                    if (properties == null) {
                        AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'",
                                project.getName());
                        return null;
                    } else {
                        //legacy mode.
                        // get a working copy with the new type "project"
                        ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy(PropertyType.PROJECT);
                        // and save it
                        try {
                            wc.save();

                            // delete the old file.
                            ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT);

                            // make sure to use the new properties
                            properties = ProjectProperties.load(projectLocation, PropertyType.PROJECT);
                        } catch (Exception e) {
                            AdtPlugin.log(IStatus.ERROR,
                                    "Failed to rename properties file to %1$s for project '%s2$'",
                                    PropertyType.PROJECT.getFilename(), project.getName());
                        }
                    }
                }

                state = new ProjectState(project, properties);
                sProjectStateMap.put(project, state);

                // try to resolve the target
                if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
                    sCurrentSdk.loadTargetAndBuildTools(state);
                }
            }

            return state;
        }
    }

    /**
     * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
     */
    @Nullable
    public IAndroidTarget getTarget(IProject project) {
        if (project == null) {
            return null;
        }

        ProjectState state = getProjectState(project);
        if (state != null) {
            return state.getTarget();
        }

        return null;
    }

    /**
     * Loads the {@link IAndroidTarget} and BuildTools for a given project.
     * <p/>This method will get the target hash string from the project properties, and resolve
     * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
     * @param state the state representing the project to load.
     * @return the target that was loaded.
     */
    @Nullable
    public IAndroidTarget loadTargetAndBuildTools(ProjectState state) {
        IAndroidTarget target = null;
        if (state != null) {
            String hash = state.getTargetHashString();
            if (hash != null) {
                state.setTarget(target = getTargetFromHashString(hash));
            }

            String markerMessage = null;
            String buildToolInfoVersion = state.getBuildToolInfoVersion();
            if (buildToolInfoVersion != null) {
                BuildToolInfo buildToolsInfo = getBuildToolInfo(buildToolInfoVersion);

                if (buildToolsInfo != null) {
                    state.setBuildToolInfo(buildToolsInfo);
                } else {
                    markerMessage = String.format("Unable to resolve %s property value '%s'",
                            ProjectProperties.PROPERTY_BUILD_TOOLS, buildToolInfoVersion);
                }
            } else {
                // this is ok, we'll use the latest one automatically.
                state.setBuildToolInfo(null);
            }

            handleBuildToolsMarker(state.getProject(), markerMessage);
        }

        return target;
    }

    /**
     * Adds or edit a build tools marker from the given project. This is done through a Job.
     * @param project the project
     * @param markerMessage the message. if null the marker is removed.
     */
    private void handleBuildToolsMarker(final IProject project, final String markerMessage) {
        Job markerJob = new Job("Android SDK: Build Tools Marker") {
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    if (project.isAccessible()) {
                        // always delete existing marker first
                        project.deleteMarkers(AdtConstants.MARKER_BUILD_TOOLS, true, IResource.DEPTH_ZERO);

                        // add the new one if needed.
                        if (markerMessage != null) {
                            BaseProjectHelper.markProject(project, AdtConstants.MARKER_BUILD_TOOLS, markerMessage,
                                    IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH);
                        }
                    }
                } catch (CoreException e2) {
                    AdtPlugin.log(e2, null);
                    // Don't return e2.getStatus(); the job control will then produce
                    // a popup with this error, which isn't very interesting for the
                    // user.
                }

                return Status.OK_STATUS;
            }
        };

        // build jobs are run after other interactive jobs
        markerJob.setPriority(Job.BUILD);
        markerJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
        markerJob.schedule();
    }

    /**
     * Checks and loads (if needed) the data for a given target.
     * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
     * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
     * <p/>An optional project as second parameter can be given to be recompiled once the target
     * data is finished loading.
     * <p/>The return value is non-null only if the target data has already been loaded (and in this
     * case is the status of the load operation)
     * @param target the target to load.
     * @param project an optional project to be recompiled when the target data is loaded.
     * If the target is already loaded, nothing happens.
     * @return The load status if the target data is already loaded.
     */
    @NonNull
    public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
        boolean loadData = false;

        synchronized (LOCK) {
            if (mDontLoadTargetData) {
                return LoadStatus.FAILED;
            }

            TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
            if (bundle == null) {
                bundle = new TargetLoadBundle();
                mTargetDataStatusMap.put(target, bundle);

                // set status to loading
                bundle.status = LoadStatus.LOADING;

                // add project to bundle
                if (project != null) {
                    bundle.projectsToReload.add(project);
                }

                // and set the flag to start the loading below
                loadData = true;
            } else if (bundle.status == LoadStatus.LOADING) {
                // add project to bundle
                if (project != null) {
                    bundle.projectsToReload.add(project);
                }

                return bundle.status;
            } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
                return bundle.status;
            }
        }

        if (loadData) {
            Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
                @Override
                protected IStatus run(IProgressMonitor monitor) {
                    AdtPlugin plugin = AdtPlugin.getDefault();
                    try {
                        IStatus status = new AndroidTargetParser(target).run(monitor);

                        IJavaProject[] javaProjectArray = null;

                        synchronized (LOCK) {
                            TargetLoadBundle bundle = mTargetDataStatusMap.get(target);

                            if (status.getCode() != IStatus.OK) {
                                bundle.status = LoadStatus.FAILED;
                                bundle.projectsToReload.clear();
                            } else {
                                bundle.status = LoadStatus.LOADED;

                                // Prepare the array of project to recompile.
                                // The call is done outside of the synchronized block.
                                javaProjectArray = bundle.projectsToReload
                                        .toArray(new IJavaProject[bundle.projectsToReload.size()]);

                                // and update the UI of the editors that depend on the target data.
                                plugin.updateTargetListeners(target);
                            }
                        }

                        if (javaProjectArray != null) {
                            ProjectHelper.updateProjects(javaProjectArray);
                        }

                        return status;
                    } catch (Throwable t) {
                        synchronized (LOCK) {
                            TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
                            bundle.status = LoadStatus.FAILED;
                        }

                        AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$
                        String message = String.format("Parsing Data for %1$s failed", target.hashString());
                        if (t instanceof UnsupportedClassVersionError) {
                            message = "To use this platform, run Eclipse with JDK 7 or later. (" + message + ")";
                        }
                        return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, t);
                    }
                }
            };
            job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
            job.setRule(ResourcesPlugin.getWorkspace().getRoot());
            job.schedule();
        }

        // The only way to go through here is when the loading starts through the Job.
        // Therefore the current status of the target is LOADING.
        return LoadStatus.LOADING;
    }

    /**
     * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
     */
    @Nullable
    public AndroidTargetData getTargetData(IAndroidTarget target) {
        synchronized (LOCK) {
            return mTargetDataMap.get(target);
        }
    }

    /**
     * Return the {@link AndroidTargetData} for a given {@link IProject}.
     */
    @Nullable
    public AndroidTargetData getTargetData(IProject project) {
        synchronized (LOCK) {
            IAndroidTarget target = getTarget(project);
            if (target != null) {
                return getTargetData(target);
            }
        }

        return null;
    }

    /**
     * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not
     * loaded properly, then this will return <code>null</code>.
     */
    @Nullable
    public DexWrapper getDexWrapper(@Nullable BuildToolInfo buildToolInfo) {
        if (buildToolInfo == null) {
            return null;
        }
        synchronized (LOCK) {
            String dexLocation = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
            DexWrapper dexWrapper = mDexWrappers.get(dexLocation);

            if (dexWrapper == null) {
                // load DX.
                dexWrapper = new DexWrapper();
                IStatus res = dexWrapper.loadDex(dexLocation);
                if (res != Status.OK_STATUS) {
                    AdtPlugin.log(null, res.getMessage());
                    dexWrapper = null;
                } else {
                    mDexWrappers.put(dexLocation, dexWrapper);
                }
            }

            return dexWrapper;
        }
    }

    public void unloadDexWrappers() {
        synchronized (LOCK) {
            for (DexWrapper wrapper : mDexWrappers.values()) {
                wrapper.unload();
            }
            mDexWrappers.clear();
        }
    }

    /**
     * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
     * be <code>null</code>.
     */
    @Nullable
    public AvdManager getAvdManager() {
        return mAvdManager;
    }

    @Nullable
    public static AndroidVersion getDeviceVersion(@NonNull IDevice device) {
        try {
            Map<String, String> props = device.getProperties();
            String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
            if (apiLevel == null) {
                return null;
            }

            return new AndroidVersion(Integer.parseInt(apiLevel), props.get((IDevice.PROP_BUILD_CODENAME)));
        } catch (NumberFormatException e) {
            return null;
        }
    }

    @NonNull
    public DeviceManager getDeviceManager() {
        return mDeviceManager;
    }

    /**
     * Returns a list of {@link ProjectState} representing projects depending, directly or
     * indirectly on a given library project.
     * @param project the library project.
     * @return a possibly empty list of ProjectState.
     */
    @NonNull
    public static Set<ProjectState> getMainProjectsFor(IProject project) {
        synchronized (LOCK) {
            // first get the project directly depending on this.
            Set<ProjectState> list = new HashSet<ProjectState>();

            // loop on all project and see if ProjectState.getLibrary returns a non null
            // project.
            for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
                if (project != entry.getKey()) {
                    LibraryState library = entry.getValue().getLibrary(project);
                    if (library != null) {
                        list.add(entry.getValue());
                    }
                }
            }

            // now look for projects depending on the projects directly depending on the library.
            HashSet<ProjectState> result = new HashSet<ProjectState>(list);
            for (ProjectState p : list) {
                if (p.isLibrary()) {
                    Set<ProjectState> set = getMainProjectsFor(p.getProject());
                    result.addAll(set);
                }
            }

            return result;
        }
    }

    /**
     * Unload the SDK's target data.
     *
     * If <var>preventReload</var>, this effect is final until the SDK instance is changed
     * through {@link #loadSdk(String)}.
     *
     * The goal is to unload the targets to be able to replace existing targets with new ones,
     * before calling {@link #loadSdk(String)} to fully reload the SDK.
     *
     * @param preventReload prevent the data from being loaded again for the remaining live of
     *   this {@link Sdk} instance.
     */
    public void unloadTargetData(boolean preventReload) {
        synchronized (LOCK) {
            mDontLoadTargetData = preventReload;

            // dispose of the target data.
            for (AndroidTargetData data : mTargetDataMap.values()) {
                data.dispose();
            }

            mTargetDataMap.clear();
        }
    }

    private Sdk(SdkManager manager, AvdManager avdManager) {
        mManager = manager;
        mAvdManager = avdManager;

        // listen to projects closing
        GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
        // need to register the resource event listener first because the project listener
        // is called back during registration with project opened in the workspace.
        monitor.addResourceEventListener(mResourceEventListener);
        monitor.addProjectListener(mProjectListener);
        monitor.addFileListener(mFileListener,
                IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED);

        // pre-compute some paths
        mDocBaseUrl = getDocumentationBaseUrl(manager.getLocation() + SdkConstants.OS_SDK_DOCS_FOLDER);

        mDeviceManager = DeviceManager.createInstance(manager.getLocalSdk().getLocation(), AdtPlugin.getDefault());

        // update whatever ProjectState is already present with new IAndroidTarget objects.
        synchronized (LOCK) {
            for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
                loadTargetAndBuildTools(entry.getValue());
            }
        }
    }

    /**
     *  Cleans and unloads the SDK.
     */
    private void dispose() {
        GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
        monitor.removeProjectListener(mProjectListener);
        monitor.removeFileListener(mFileListener);
        monitor.removeResourceEventListener(mResourceEventListener);

        // the IAndroidTarget objects are now obsolete so update the project states.
        synchronized (LOCK) {
            for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
                entry.getValue().setTarget(null);
            }

            // dispose of the target data.
            for (AndroidTargetData data : mTargetDataMap.values()) {
                data.dispose();
            }

            mTargetDataMap.clear();
        }
    }

    void setTargetData(IAndroidTarget target, AndroidTargetData data) {
        synchronized (LOCK) {
            mTargetDataMap.put(target, data);
        }
    }

    /**
     * Returns the URL to the local documentation.
     * Can return null if no documentation is found in the current SDK.
     *
     * @param osDocsPath Path to the documentation folder in the current SDK.
     *  The folder may not actually exist.
     * @return A file:// URL on the local documentation folder if it exists or null.
     */
    private String getDocumentationBaseUrl(String osDocsPath) {
        File f = new File(osDocsPath);

        if (f.isDirectory()) {
            try {
                // Note: to create a file:// URL, one would typically use something like
                // f.toURI().toURL().toString(). However this generates a broken path on
                // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
                // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
                // do the correct thing manually.

                String path = f.getAbsolutePath();
                if (File.separatorChar != '/') {
                    path = path.replace(File.separatorChar, '/');
                }

                // For some reason the URL class doesn't add the mandatory "//" after
                // the "file:" protocol name, so it has to be hacked into the path.
                URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$
                String result = url.toString();
                return result;
            } catch (MalformedURLException e) {
                // ignore malformed URLs
            }
        }

        return null;
    }

    /**
     * Delegate listener for project changes.
     */
    private IProjectListener mProjectListener = new IProjectListener() {
        @Override
        public void projectClosed(IProject project) {
            onProjectRemoved(project, false /*deleted*/);
        }

        @Override
        public void projectDeleted(IProject project) {
            onProjectRemoved(project, true /*deleted*/);
        }

        private void onProjectRemoved(IProject removedProject, boolean deleted) {
            if (DEBUG) {
                System.out.println(">>> CLOSED: " + removedProject.getName());
            }

            // get the target project
            synchronized (LOCK) {
                // Don't use getProject() as it could create the ProjectState if it's not
                // there yet and this is not what we want. We want the current object.
                // Therefore, direct access to the map.
                ProjectState removedState = sProjectStateMap.get(removedProject);
                if (removedState != null) {
                    // 1. clear the layout lib cache associated with this project
                    IAndroidTarget target = removedState.getTarget();
                    if (target != null) {
                        // get the bridge for the target, and clear the cache for this project.
                        AndroidTargetData data = mTargetDataMap.get(target);
                        if (data != null) {
                            LayoutLibrary layoutLib = data.getLayoutLibrary();
                            if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) {
                                layoutLib.clearCaches(removedProject);
                            }
                        }
                    }

                    // 2. if the project is a library, make sure to update the
                    // LibraryState for any project referencing it.
                    // Also, record the updated projects that are libraries, to update
                    // projects that depend on them.
                    for (ProjectState projectState : sProjectStateMap.values()) {
                        LibraryState libState = projectState.getLibrary(removedProject);
                        if (libState != null) {
                            // Close the library right away.
                            // This remove links between the LibraryState and the projectState.
                            // This is because in case of a rename of a project, projectClosed and
                            // projectOpened will be called before any other job is run, so we
                            // need to make sure projectOpened is closed with the main project
                            // state up to date.
                            libState.close();

                            // record that this project changed, and in case it's a library
                            // that its parents need to be updated as well.
                            markProject(projectState, projectState.isLibrary());
                        }
                    }

                    // now remove the project for the project map.
                    sProjectStateMap.remove(removedProject);
                }
            }

            if (DEBUG) {
                System.out.println("<<<");
            }
        }

        @Override
        public void projectOpened(IProject project) {
            onProjectOpened(project);
        }

        @Override
        public void projectOpenedWithWorkspace(IProject project) {
            // no need to force recompilation when projects are opened with the workspace.
            onProjectOpened(project);
        }

        @Override
        public void allProjectsOpenedWithWorkspace() {
            // Correct currently open editors
            fixOpenLegacyEditors();
        }

        private void onProjectOpened(final IProject openedProject) {

            ProjectState openedState = getProjectState(openedProject);
            if (openedState != null) {
                if (DEBUG) {
                    System.out.println(">>> OPENED: " + openedProject.getName());
                }

                synchronized (LOCK) {
                    final boolean isLibrary = openedState.isLibrary();
                    final boolean hasLibraries = openedState.hasLibraries();

                    if (isLibrary || hasLibraries) {
                        boolean foundLibraries = false;
                        // loop on all the existing project and update them based on this new
                        // project
                        for (ProjectState projectState : sProjectStateMap.values()) {
                            if (projectState != openedState) {
                                // If the project has libraries, check if this project
                                // is a reference.
                                if (hasLibraries) {
                                    // ProjectState#needs() both checks if this is a missing library
                                    // and updates LibraryState to contains the new values.
                                    // This must always be called.
                                    LibraryState libState = openedState.needs(projectState);

                                    if (libState != null) {
                                        // found a library! Add the main project to the list of
                                        // modified project
                                        foundLibraries = true;
                                    }
                                }

                                // if the project is a library check if the other project depend
                                // on it.
                                if (isLibrary) {
                                    // ProjectState#needs() both checks if this is a missing library
                                    // and updates LibraryState to contains the new values.
                                    // This must always be called.
                                    LibraryState libState = projectState.needs(openedState);

                                    if (libState != null) {
                                        // There's a dependency! Add the project to the list of
                                        // modified project, but also to a list of projects
                                        // that saw one of its dependencies resolved.
                                        markProject(projectState, projectState.isLibrary());
                                    }
                                }
                            }
                        }

                        // if the project has a libraries and we found at least one, we add
                        // the project to the list of modified project.
                        // Since we already went through the parent, no need to update them.
                        if (foundLibraries) {
                            markProject(openedState, false /*updateParents*/);
                        }
                    }
                }

                // Correct file editor associations.
                fixEditorAssociations(openedProject);

                // Fix classpath entries in a job since the workspace might be locked now.
                Job fixCpeJob = new Job("Adjusting Android Project Classpath") {
                    @Override
                    protected IStatus run(IProgressMonitor monitor) {
                        try {
                            ProjectHelper.fixProjectClasspathEntries(JavaCore.create(openedProject));
                        } catch (JavaModelException e) {
                            AdtPlugin.log(e, "error fixing classpath entries");
                            // Don't return e2.getStatus(); the job control will then produce
                            // a popup with this error, which isn't very interesting for the
                            // user.
                        }

                        return Status.OK_STATUS;
                    }
                };

                // build jobs are run after other interactive jobs
                fixCpeJob.setPriority(Job.BUILD);
                fixCpeJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
                fixCpeJob.schedule();

                if (DEBUG) {
                    System.out.println("<<<");
                }
            }
        }

        @Override
        public void projectRenamed(IProject project, IPath from) {
            // we don't actually care about this anymore.
        }
    };

    /**
     * Delegate listener for file changes.
     */
    private IFileListener mFileListener = new IFileListener() {
        @Override
        public void fileChanged(final @NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, int kind,
                @Nullable String extension, int flags, boolean isAndroidPRoject) {
            if (!isAndroidPRoject) {
                return;
            }

            if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName())
                    && file.getParent() == file.getProject()) {
                try {
                    // reload the content of the project.properties file and update
                    // the target.
                    IProject iProject = file.getProject();

                    ProjectState state = Sdk.getProjectState(iProject);

                    // get the current target and build tools
                    IAndroidTarget oldTarget = state.getTarget();
                    boolean oldRsSupportMode = state.getRenderScriptSupportMode();

                    // get the current library flag
                    boolean wasLibrary = state.isLibrary();

                    LibraryDifference diff = state.reloadProperties();

                    // load the (possibly new) target.
                    IAndroidTarget newTarget = loadTargetAndBuildTools(state);

                    // reload the libraries if needed
                    if (diff.hasDiff()) {
                        if (diff.added) {
                            synchronized (LOCK) {
                                for (ProjectState projectState : sProjectStateMap.values()) {
                                    if (projectState != state) {
                                        // need to call needs to do the libraryState link,
                                        // but no need to look at the result, as we'll compare
                                        // the result of getFullLibraryProjects()
                                        // this is easier to due to indirect dependencies.
                                        state.needs(projectState);
                                    }
                                }
                            }
                        }

                        markProject(state, wasLibrary || state.isLibrary());
                    }

                    // apply the new target if needed.
                    if (newTarget != oldTarget || oldRsSupportMode != state.getRenderScriptSupportMode()) {
                        IJavaProject javaProject = BaseProjectHelper.getJavaProject(file.getProject());
                        if (javaProject != null) {
                            ProjectHelper.updateProject(javaProject);
                        }

                        // update the editors to reload with the new target
                        AdtPlugin.getDefault().updateTargetListeners(iProject);
                    }
                } catch (CoreException e) {
                    // This can't happen as it's only for closed project (or non existing)
                    // but in that case we can't get a fileChanged on this file.
                }
            } else if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) {
                // check if it's an add/remove on a jar files inside libs
                if (EXT_JAR.equals(extension) && file.getProjectRelativePath().segmentCount() == 2
                        && file.getParent().getName().equals(SdkConstants.FD_NATIVE_LIBS)) {
                    // need to update the project and whatever depend on it.

                    processJarFileChange(file);
                }
            }
        }

        private void processJarFileChange(final IFile file) {
            try {
                IProject iProject = file.getProject();

                if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
                    return;
                }

                List<IJavaProject> projectList = new ArrayList<IJavaProject>();
                IJavaProject javaProject = BaseProjectHelper.getJavaProject(iProject);
                if (javaProject != null) {
                    projectList.add(javaProject);
                }

                ProjectState state = Sdk.getProjectState(iProject);

                if (state != null) {
                    Collection<ProjectState> parents = state.getFullParentProjects();
                    for (ProjectState s : parents) {
                        javaProject = BaseProjectHelper.getJavaProject(s.getProject());
                        if (javaProject != null) {
                            projectList.add(javaProject);
                        }
                    }

                    ProjectHelper.updateProjects(projectList.toArray(new IJavaProject[projectList.size()]));
                }
            } catch (CoreException e) {
                // This can't happen as it's only for closed project (or non existing)
                // but in that case we can't get a fileChanged on this file.
            }
        }
    };

    /** List of modified projects. This is filled in
     * {@link IProjectListener#projectOpened(IProject)},
     * {@link IProjectListener#projectOpenedWithWorkspace(IProject)},
     * {@link IProjectListener#projectClosed(IProject)}, and
     * {@link IProjectListener#projectDeleted(IProject)} and processed in
     * {@link IResourceEventListener#resourceChangeEventEnd()}.
     */
    private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>();
    private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>();

    private void markProject(ProjectState projectState, boolean updateParents) {
        if (mModifiedProjects.contains(projectState) == false) {
            if (DEBUG) {
                System.out.println("\tMARKED: " + projectState.getProject().getName());
            }
            mModifiedProjects.add(projectState);
        }

        // if the project is resolved also add it to this list.
        if (updateParents) {
            if (mModifiedChildProjects.contains(projectState) == false) {
                if (DEBUG) {
                    System.out.println("\tMARKED(child): " + projectState.getProject().getName());
                }
                mModifiedChildProjects.add(projectState);
            }
        }
    }

    /**
     * Delegate listener for resource changes. This is called before and after any calls to the
     * project and file listeners (for a given resource change event).
     */
    private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
        @Override
        public void resourceChangeEventStart() {
            mModifiedProjects.clear();
            mModifiedChildProjects.clear();
        }

        @Override
        public void resourceChangeEventEnd() {
            if (mModifiedProjects.size() == 0) {
                return;
            }

            // first make sure all the parents are updated
            updateParentProjects();

            // for all modified projects, update their library list
            // and gather their IProject
            final List<IJavaProject> projectList = new ArrayList<IJavaProject>();
            for (ProjectState state : mModifiedProjects) {
                state.updateFullLibraryList();
                projectList.add(JavaCore.create(state.getProject()));
            }

            Job job = new Job("Android Library Update") { //$NON-NLS-1$
                @Override
                protected IStatus run(IProgressMonitor monitor) {
                    LibraryClasspathContainerInitializer
                            .updateProjects(projectList.toArray(new IJavaProject[projectList.size()]));

                    for (IJavaProject javaProject : projectList) {
                        try {
                            javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, monitor);
                        } catch (CoreException e) {
                            // pass
                        }
                    }
                    return Status.OK_STATUS;
                }
            };
            job.setPriority(Job.BUILD);
            job.setRule(ResourcesPlugin.getWorkspace().getRoot());
            job.schedule();
        }
    };

    /**
     * Updates all existing projects with a given list of new/updated libraries.
     * This loops through all opened projects and check if they depend on any of the given
     * library project, and if they do, they are linked together.
     */
    private void updateParentProjects() {
        if (mModifiedChildProjects.size() == 0) {
            return;
        }

        ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects);
        mModifiedChildProjects.clear();
        synchronized (LOCK) {
            // for each project for which we must update its parent, we loop on the parent
            // projects and adds them to the list of modified projects. If they are themselves
            // libraries, we add them too.
            for (ProjectState state : childProjects) {
                if (DEBUG) {
                    System.out.println(">>> Updating parents of " + state.getProject().getName());
                }
                List<ProjectState> parents = state.getParentProjects();
                for (ProjectState parent : parents) {
                    markProject(parent, parent.isLibrary());
                }
                if (DEBUG) {
                    System.out.println("<<<");
                }
            }
        }

        // done, but there may be parents that are also libraries. Need to update their parents.
        updateParentProjects();
    }

    /**
     * Fix editor associations for the given project, if not already done.
     * <p/>
     * Eclipse has a per-file setting for which editor should be used for each file
     * (see {@link IDE#setDefaultEditor(IFile, String)}).
     * We're using this flag to pick between the various XML editors (layout, drawable, etc)
     * since they all have the same file name extension.
     * <p/>
     * Unfortunately, the file setting can be "wrong" for two reasons:
     * <ol>
     *   <li> The editor type was added <b>after</b> a file had been seen by the IDE.
     *        For example, we added new editors for animations and for drawables around
     *        ADT 12, but any file seen by ADT in earlier versions will continue to use
     *        the vanilla Eclipse XML editor instead.
     *   <li> A bug in ADT 14 and ADT 15 (see issue 21124) meant that files created in new
     *        folders would end up with wrong editor associations. Even though that bug
     *        is fixed in ADT 16, the fix only affects new files, it cannot retroactively
     *        fix editor associations that were set incorrectly by ADT 14 or 15.
     * </ol>
     * <p/>
     * This method attempts to fix the editor bindings retroactively by scanning all the
     * resource XML files and resetting the editor associations.
     * Since this is a potentially slow operation, this is only done "once"; we use a
     * persistent project property to avoid looking repeatedly. In the future if we add
     * additional editors, we can rev the scanned version value.
     */
    private void fixEditorAssociations(final IProject project) {
        QualifiedName KEY = new QualifiedName(AdtPlugin.PLUGIN_ID, "editorbinding"); //$NON-NLS-1$

        try {
            String value = project.getPersistentProperty(KEY);
            int currentVersion = 0;
            if (value != null) {
                try {
                    currentVersion = Integer.parseInt(value);
                } catch (Exception ingore) {
                }
            }

            // The target version we're comparing to. This must be incremented each time
            // we change the processing here so that a new version of the plugin would
            // try to fix existing user projects.
            final int targetVersion = 2;

            if (currentVersion >= targetVersion) {
                return;
            }

            // Set to specific version such that we can rev the version in the future
            // to trigger further scanning
            project.setPersistentProperty(KEY, Integer.toString(targetVersion));

            // Now update the actual editor associations.
            Job job = new Job("Update Android editor bindings") { //$NON-NLS-1$
                @Override
                protected IStatus run(IProgressMonitor monitor) {
                    try {
                        for (IResource folderResource : project.getFolder(FD_RES).members()) {
                            if (folderResource instanceof IFolder) {
                                IFolder folder = (IFolder) folderResource;

                                for (IResource resource : folder.members()) {
                                    if (resource instanceof IFile && resource.getName().endsWith(DOT_XML)) {
                                        fixXmlFile((IFile) resource);
                                    }
                                }
                            }
                        }

                        // TODO change AndroidManifest.xml ID too

                    } catch (CoreException e) {
                        AdtPlugin.log(e, null);
                    }

                    return Status.OK_STATUS;
                }

                /**
                 * Attempt to fix the editor ID for the given /res XML file.
                 */
                private void fixXmlFile(final IFile file) {
                    // Fix the default editor ID for this resource.
                    // This has no effect on currently open editors.
                    IEditorDescriptor desc = IDE.getDefaultEditor(file);

                    if (desc == null || !CommonXmlEditor.ID.equals(desc.getId())) {
                        IDE.setDefaultEditor(file, CommonXmlEditor.ID);
                    }
                }
            };
            job.setPriority(Job.BUILD);
            job.schedule();
        } catch (CoreException e) {
            AdtPlugin.log(e, null);
        }
    }

    /**
     * Tries to fix all currently open Android legacy editors.
     * <p/>
     * If an editor is found to match one of the legacy ids, we'll try to close it.
     * If that succeeds, we try to reopen it using the new common editor ID.
     * <p/>
     * This method must be run from the UI thread.
     */
    private void fixOpenLegacyEditors() {

        AdtPlugin adt = AdtPlugin.getDefault();
        if (adt == null) {
            return;
        }

        final IPreferenceStore store = adt.getPreferenceStore();
        int currentValue = store.getInt(AdtPrefs.PREFS_FIX_LEGACY_EDITORS);
        // The target version we're comparing to. This must be incremented each time
        // we change the processing here so that a new version of the plugin would
        // try to fix existing editors.
        final int targetValue = 1;

        if (currentValue >= targetValue) {
            return;
        }

        // To be able to close and open editors we need to make sure this is done
        // in the UI thread, which this isn't invoked from.
        PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
            @Override
            public void run() {
                HashSet<String> legacyIds = new HashSet<String>(Arrays.asList(CommonXmlEditor.LEGACY_EDITOR_IDS));

                for (IWorkbenchWindow win : PlatformUI.getWorkbench().getWorkbenchWindows()) {
                    for (IWorkbenchPage page : win.getPages()) {
                        for (IEditorReference ref : page.getEditorReferences()) {
                            try {
                                IEditorInput input = ref.getEditorInput();
                                if (input instanceof IFileEditorInput) {
                                    IFile file = ((IFileEditorInput) input).getFile();
                                    IEditorPart part = ref.getEditor(true /*restore*/);
                                    if (part != null) {
                                        IWorkbenchPartSite site = part.getSite();
                                        if (site != null) {
                                            String id = site.getId();
                                            if (legacyIds.contains(id)) {
                                                // This editor matches one of legacy editor IDs.
                                                fixEditor(page, part, input, file, id);
                                            }
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                // ignore
                            }
                        }
                    }
                }

                // Remember that we managed to do fix all editors
                store.setValue(AdtPrefs.PREFS_FIX_LEGACY_EDITORS, targetValue);
            }

            private void fixEditor(IWorkbenchPage page, IEditorPart part, IEditorInput input, IFile file,
                    String id) {
                IDE.setDefaultEditor(file, CommonXmlEditor.ID);

                boolean ok = page.closeEditor(part, true /*save*/);

                AdtPlugin.log(IStatus.INFO, "Closed legacy editor ID %s for %s: %s", //$NON-NLS-1$
                        id, file.getFullPath(), ok ? "Success" : "Failed");//$NON-NLS-1$ //$NON-NLS-2$

                if (ok) {
                    // Try to reopen it with the new ID
                    try {
                        page.openEditor(input, CommonXmlEditor.ID);
                    } catch (PartInitException e) {
                        AdtPlugin.log(e, "Failed to reopen %s", //$NON-NLS-1$
                                file.getFullPath());
                    }
                }
            }
        });
    }
}