/* * 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 com.android.ddmlib.IDevice; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 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.sdk.AndroidTargetData.LayoutBridge; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.ISdkLog; import com.android.sdklib.SdkConstants; import com.android.sdklib.SdkManager; import com.android.sdklib.internal.avd.AvdManager; import com.android.sdklib.internal.project.ApkConfigurationHelper; import com.android.sdklib.internal.project.ApkSettings; import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IncrementalProjectBuilder; 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.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; /** * 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 class Sdk implements IProjectListener, IFileListener { private static Sdk sCurrentSdk = null; private final SdkManager mManager; private final AvdManager mAvdManager; /** * 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> projecsToReload = new HashSet<IJavaProject>(); } /** 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>(); /** Map associating {@link IProject} and their resolved {@link IAndroidTarget}. */ private final HashMap<IProject, IAndroidTarget> mProjectTargetMap = new HashMap<IProject, IAndroidTarget>(); /** Map associating {@link IProject} and their APK creation settings ({@link ApkSettings}). */ private final HashMap<IProject, ApkSettings> mApkSettingsMap = new HashMap<IProject, ApkSettings>(); private final String mDocBaseUrl; private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager(); /** * 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(); public void onProjectTargetChange(IProject changedProject) { if (changedProject != null && changedProject.equals(getProject())) { reload(); } } public void onTargetLoaded(IAndroidTarget target) { IProject project = getProject(); if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) { reload(); } } public void onSdkLoaded() { // do nothing; } } /** * 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. */ public static synchronized Sdk loadSdk(String sdkLocation) { if (sCurrentSdk != null) { sCurrentSdk.dispose(); sCurrentSdk = null; } final ArrayList<String> logMessages = new ArrayList<String>(); ISdkLog log = new ISdkLog() { public void error(Throwable throwable, String errorFormat, Object... arg) { if (errorFormat != null) { logMessages.add(String.format("Error: " + errorFormat, arg)); } if (throwable != null) { logMessages.add(throwable.getMessage()); } } public void warning(String warningFormat, Object... arg) { logMessages.add(String.format("Warning: " + warningFormat, arg)); } public void printf(String msgFormat, Object... arg) { logMessages.add(String.format(msgFormat, arg)); } }; // get an SdkManager object for the location SdkManager manager = SdkManager.createManager(sdkLocation, log); if (manager != null) { AvdManager avdManager = null; try { avdManager = new AvdManager(manager, log); } catch (AndroidLocationException e) { log.error(e, "Error parsing the AVDs"); } sCurrentSdk = new Sdk(manager, avdManager); return sCurrentSdk; } else { StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); for (String msg : logMessages) { sb.append('\n'); sb.append(msg); } AdtPlugin.displayError("Android SDK", sb.toString()); } return null; } /** * Returns the current {@link Sdk} object. */ public static synchronized Sdk getCurrent() { return sCurrentSdk; } /** * Returns the location (OS path) of the current SDK. */ public String getSdkLocation() { return mManager.getLocation(); } /** * 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. */ public String getDocumentationBaseUrl() { return mDocBaseUrl; } /** * Returns the list of targets that are available in the SDK. */ public IAndroidTarget[] getTargets() { return mManager.getTargets(); } /** * 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. */ public IAndroidTarget getTargetFromHashString(String hash) { return mManager.getTargetFromHashString(hash); } /** * Sets a new target and a new list of Apk configuration for a given project. * * @param project the project to receive the new apk configurations * @param target The new target to set, or <code>null</code> to not change the current target. * @param apkConfigMap a map of apk configurations. The map contains (name, filter) where name * is the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of * resource configuration to include in the apk (see aapt -c). Can be <code>null</code> if the * apk configurations should not be updated. */ public void setProject(IProject project, IAndroidTarget target, ApkSettings settings) { synchronized (AdtPlugin.getDefault().getSdkLockObject()) { boolean resolveProject = false; ProjectProperties properties = ProjectProperties.load( project.getLocation().toOSString(), PropertyType.DEFAULT); if (properties == null) { // doesn't exist yet? we create it. properties = ProjectProperties.create(project.getLocation().toOSString(), PropertyType.DEFAULT); } if (target != null) { // look for the current target of the project IAndroidTarget previousTarget = mProjectTargetMap.get(project); if (target != previousTarget) { // save the target hash string in the project persistent property properties.setAndroidTarget(target); // put it in a local map for easy access. mProjectTargetMap.put(project, target); resolveProject = true; } } // if there's no settings, force default values (to reset possibly changed // values in a previous call. if (settings == null) { settings = new ApkSettings(); } // save the project settings into the project persistent property ApkConfigurationHelper.setProperties(properties, settings); // put it in a local map for easy access. mApkSettingsMap.put(project, settings); // we are done with the modification. Save the property file. try { properties.save(); } catch (IOException e) { AdtPlugin.log(e, "Failed to save default.properties for project '%s'", project.getName()); } if (resolveProject) { // force a resolve of the project by updating the classpath container. // This will also force a recompile. IJavaProject javaProject = JavaCore.create(project); AndroidClasspathContainerInitializer.updateProjects( new IJavaProject[] { javaProject }); } else { // always do a full clean/build. try { project.build(IncrementalProjectBuilder.CLEAN_BUILD, null); } catch (CoreException e) { // failed to build? force resolve instead. IJavaProject javaProject = JavaCore.create(project); AndroidClasspathContainerInitializer.updateProjects( new IJavaProject[] { javaProject }); } } // finally, update the opened editors. if (resolveProject) { AdtPlugin.getDefault().updateTargetListeners(project); } } } /** * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. */ public IAndroidTarget getTarget(IProject project) { if (project == null) { return null; } synchronized (AdtPlugin.getDefault().getSdkLockObject()) { IAndroidTarget target = mProjectTargetMap.get(project); if (target == null) { // get the value from the project persistent property. String targetHashString = loadProjectProperties(project, this); if (targetHashString != null) { target = mManager.getTargetFromHashString(targetHashString); } } return target; } } /** * Parses the project properties and returns the hash string uniquely identifying the * target of the given project. * <p/> * This methods reads the content of the <code>default.properties</code> file present in * the root folder of the project. * <p/>The returned string is equivalent to the return of {@link IAndroidTarget#hashString()}. * @param project The project for which to return the target hash string. * @param sdkStorage The sdk in which to store the Apk Configs. Can be null. * @return the hash string or null if the project does not have a target set. */ private static String loadProjectProperties(IProject project, Sdk sdkStorage) { // load the default.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; } ProjectProperties properties = ProjectProperties.load(location.toOSString(), PropertyType.DEFAULT); if (properties == null) { AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'", project.getName()); return null; } if (sdkStorage != null) { synchronized (AdtPlugin.getDefault().getSdkLockObject()) { ApkSettings settings = ApkConfigurationHelper.getSettings(properties); if (settings != null) { sdkStorage.mApkSettingsMap.put(project, settings); } } } return properties.getProperty(ProjectProperties.PROPERTY_TARGET); } /** * Returns the hash string uniquely identifying the target of a project. * <p/> * This methods reads the content of the <code>default.properties</code> file present in * the root folder of the project. * <p/>The string is equivalent to the return of {@link IAndroidTarget#hashString()}. * @param project The project for which to return the target hash string. * @return the hash string or null if the project does not have a target set. */ public static String getProjectTargetHashString(IProject project) { return loadProjectProperties(project, null /*storeConfigs*/); } /** * Sets a target hash string in given project's <code>default.properties</code> file. * @param project The project in which to save the hash string. * @param targetHashString The target hash string to save. This must be the result from * {@link IAndroidTarget#hashString()}. */ public static void setProjectTargetHashString(IProject project, String targetHashString) { // because we don't want to erase other properties from default.properties, we first load // them ProjectProperties properties = ProjectProperties.load(project.getLocation().toOSString(), PropertyType.DEFAULT); if (properties == null) { // doesn't exist yet? we create it. properties = ProjectProperties.create(project.getLocation().toOSString(), PropertyType.DEFAULT); } // add/change the target hash string. properties.setProperty(ProjectProperties.PROPERTY_TARGET, targetHashString); // and rewrite the file. try { properties.save(); } catch (IOException e) { AdtPlugin.log(e, "Failed to save default.properties for project '%s'", project.getName()); } } /** * 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. */ public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) { boolean loadData = false; synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 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.projecsToReload.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.projecsToReload.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 (plugin.getSdkLockObject()) { TargetLoadBundle bundle = mTargetDataStatusMap.get(target); if (status.getCode() != IStatus.OK) { bundle.status = LoadStatus.FAILED; bundle.projecsToReload.clear(); } else { bundle.status = LoadStatus.LOADED; // Prepare the array of project to recompile. // The call is done outside of the synchronized block. javaProjectArray = bundle.projecsToReload.toArray( new IJavaProject[bundle.projecsToReload.size()]); // and update the UI of the editors that depend on the target data. plugin.updateTargetListeners(target); } } if (javaProjectArray != null) { AndroidClasspathContainerInitializer.updateProjects(javaProjectArray); } return status; } catch (Throwable t) { synchronized (plugin.getSdkLockObject()) { TargetLoadBundle bundle = mTargetDataStatusMap.get(target); bundle.status = LoadStatus.FAILED; } AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format( "Parsing Data for %1$s failed", //$NON-NLS-1$ target.hashString()), t); } } }; job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs 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}. */ public AndroidTargetData getTargetData(IAndroidTarget target) { synchronized (AdtPlugin.getDefault().getSdkLockObject()) { return mTargetDataMap.get(target); } } /** * Return the {@link AndroidTargetData} for a given {@link IProject}. */ public AndroidTargetData getTargetData(IProject project) { synchronized (AdtPlugin.getDefault().getSdkLockObject()) { IAndroidTarget target = getTarget(project); if (target != null) { return getTargetData(target); } } return null; } /** * Returns the APK settings for a given project. */ public ApkSettings getApkSettings(IProject project) { synchronized (AdtPlugin.getDefault().getSdkLockObject()) { return mApkSettingsMap.get(project); } } /** * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could * be <code>null</code>. */ public AvdManager getAvdManager() { return mAvdManager; } public static AndroidVersion getDeviceVersion(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; } } public LayoutDeviceManager getLayoutDeviceManager() { return mLayoutDeviceManager; } private Sdk(SdkManager manager, AvdManager avdManager) { mManager = manager; mAvdManager = avdManager; // listen to projects closing GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); monitor.addProjectListener(this); monitor.addFileListener(this, IResourceDelta.CHANGED); // pre-compute some paths mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + SdkConstants.OS_SDK_DOCS_FOLDER); // load the built-in and user layout devices mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation()); // and the ones from the add-on loadLayoutDevices(); } /** * Cleans and unloads the SDK. */ private void dispose() { GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); monitor.removeProjectListener(this); monitor.removeFileListener(this); } void setTargetData(IAndroidTarget target, AndroidTargetData data) { synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 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; } /** * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to * load {@link LayoutDevice} from them. */ private void loadLayoutDevices() { IAndroidTarget[] targets = mManager.getTargets(); for (IAndroidTarget target : targets) { if (target.isPlatform() == false) { File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML); if (deviceXml.isFile()) { mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml); } } } mLayoutDeviceManager.sealAddonLayoutDevices(); } public void projectClosed(IProject project) { // get the target project synchronized (AdtPlugin.getDefault().getSdkLockObject()) { IAndroidTarget target = mProjectTargetMap.get(project); if (target != null) { // get the bridge for the target, and clear the cache for this project. AndroidTargetData data = mTargetDataMap.get(target); if (data != null) { LayoutBridge bridge = data.getLayoutBridge(); if (bridge != null && bridge.status == LoadStatus.LOADED) { bridge.bridge.clearCaches(project); } } } // now remove the project for the maps. mProjectTargetMap.remove(project); mApkSettingsMap.remove(project); } } public void projectDeleted(IProject project) { projectClosed(project); } public void projectOpened(IProject project) { // ignore this. The project will be added to the map the first time the target needs // to be resolved. } public void projectOpenedWithWorkspace(IProject project) { // ignore this. The project will be added to the map the first time the target needs // to be resolved. } public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) { if (SdkConstants.FN_DEFAULT_PROPERTIES.equals(file.getName()) && file.getParent() == file.getProject()) { // we can't do the change from the Workspace resource change notification // so we create build-type job for it. Job job = new Job("Project Update") { @Override protected IStatus run(IProgressMonitor monitor) { try { IJavaProject javaProject = BaseProjectHelper.getJavaProject( file.getProject()); if (javaProject != null) { AndroidClasspathContainerInitializer.updateProjects( new IJavaProject[] { javaProject }); } } 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. } return Status.OK_STATUS; } }; job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs job.schedule(); } } }