/* * 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}. *

This MUST NOT be accessed directly. Instead use {@link #getProjectState(IProject)}. */ private final static HashMap sProjectStateMap = new HashMap(); /** * Data bundled using during the load of Target data. *

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 projectsToReload = new HashSet(); } private final SdkManager mManager; private final Map mDexWrappers = Maps.newHashMap(); private final AvdManager mAvdManager; private final DeviceManager mDeviceManager; /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */ private final HashMap mTargetDataMap = new HashMap(); /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */ private final HashMap mTargetDataStatusMap = new HashMap(); /** * 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. *

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 logMessages = new ArrayList(); 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. *

* 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 new {@link SdkManager} that can parse the SDK located * at the current {@link #getSdkOsLocation()}. *

* 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. *

* This is just a convenient method equivalent to writing: *

SdkManager.createManager(Sdk.getCurrent().getSdkLocation(), log);
* * @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. *

* 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 project.properties * 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. *

* 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. *

Because this methods deals with projects, it's not linked to an actual {@link Sdk} * objects, and therefore is static. *

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. *

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. *

The data is loaded in a separate {@link Job}, and opened editors will be notified * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}. *

An optional project as second parameter can be given to be recompiled once the target * data is finished loading. *

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 null. */ @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 null. */ @Nullable public AvdManager getAvdManager() { return mAvdManager; } @Nullable public static AndroidVersion getDeviceVersion(@NonNull IDevice device) { try { Map 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 getMainProjectsFor(IProject project) { synchronized (LOCK) { // first get the project directly depending on this. Set list = new HashSet(); // loop on all project and see if ProjectState.getLibrary returns a non null // project. for (Entry 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 result = new HashSet(list); for (ProjectState p : list) { if (p.isLibrary()) { Set set = getMainProjectsFor(p.getProject()); result.addAll(set); } } return result; } } /** * Unload the SDK's target data. * * If preventReload, 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 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 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 projectList = new ArrayList(); IJavaProject javaProject = BaseProjectHelper.getJavaProject(iProject); if (javaProject != null) { projectList.add(javaProject); } ProjectState state = Sdk.getProjectState(iProject); if (state != null) { Collection 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 mModifiedProjects = new ArrayList(); private final List mModifiedChildProjects = new ArrayList(); 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 projectList = new ArrayList(); 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 childProjects = new ArrayList(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 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. *

* 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. *

* Unfortunately, the file setting can be "wrong" for two reasons: *

    *
  1. The editor type was added after 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. *
  2. 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. *
*

* 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. *

* 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. *

* 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 legacyIds = new HashSet(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()); } } } }); } }