blob: 7ff06fc409b70500b6463e75b224bb7504b67a1c [file] [log] [blame]
/*
* 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());
}
}
}
});
}
}