aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java
diff options
context:
space:
mode:
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.java1620
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());
+ }
+ }
+ }
+ });
+ }
+}