diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java | 1620 |
1 files changed, 1620 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java new file mode 100644 index 000000000..7ff06fc40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java @@ -0,0 +1,1620 @@ +/* + * 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()); + } + } + } + }); + } +} |