diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project')
17 files changed, 5760 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java new file mode 100644 index 000000000..475dd5a44 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2007 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.project; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; + +/** + * Classpath container for the Android projects. + * This supports both the System classpath and the library dependencies. + */ +class AndroidClasspathContainer implements IClasspathContainer { + + private final IClasspathEntry[] mClasspathEntry; + private final IPath mContainerPath; + private final String mName; + private final int mKind; + + /** + * Constructs the container with the {@link IClasspathEntry} representing the android + * framework jar file and the container id + * @param entries the entries representing the android framework and optional libraries. + * @param path the path containing the classpath container id. + * @param name the name of the container to display. + * @param the container kind. Can be {@link IClasspathContainer#K_DEFAULT_SYSTEM} or + * {@link IClasspathContainer#K_APPLICATION} + */ + AndroidClasspathContainer(IClasspathEntry[] entries, IPath path, String name, int kind) { + mClasspathEntry = entries; + mContainerPath = path; + mName = name; + mKind = kind; + } + + @Override + public IClasspathEntry[] getClasspathEntries() { + return mClasspathEntry; + } + + @Override + public String getDescription() { + return mName; + } + + @Override + public int getKind() { + return mKind; + } + + @Override + public IPath getPath() { + return mContainerPath; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java new file mode 100644 index 000000000..f9382c5ae --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java @@ -0,0 +1,823 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.SdkConstants; +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.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; +import com.google.common.collect.Maps; +import com.google.common.io.Closeables; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.osgi.framework.Bundle; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to + * {@link IProject}s. This removes the hard-coded path to the android.jar. + */ +public class AndroidClasspathContainerInitializer extends BaseClasspathContainerInitializer { + + public static final String NULL_API_URL = "<null>"; //$NON-NLS-1$ + + public static final String SOURCES_ZIP = "/sources.zip"; //$NON-NLS-1$ + + public static final String COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE = + "com.android.ide.eclipse.source"; //$NON-NLS-1$ + + private static final String ANDROID_API_REFERENCE = + "http://developer.android.com/reference/"; //$NON-NLS-1$ + + private final static String PROPERTY_ANDROID_API = "androidApi"; //$NON-NLS-1$ + + private final static String PROPERTY_ANDROID_SOURCE = "androidSource"; //$NON-NLS-1$ + + /** path separator to store multiple paths in a single property. This is guaranteed to not + * be in a path. + */ + private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ + + private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ + private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ + private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ + private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; + + private final static int CACHE_INDEX_JAR = 0; + private final static int CACHE_INDEX_SRC = 1; + private final static int CACHE_INDEX_DOCS_URI = 2; + private final static int CACHE_INDEX_OPT_DOCS_URI = 3; + private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; + + public AndroidClasspathContainerInitializer() { + // pass + } + + /** + * Binds a classpath container to a {@link IClasspathContainer} for a given project, + * or silently fails if unable to do so. + * @param containerPath the container path that is the container id. + * @param project the project to bind + */ + @Override + public void initialize(IPath containerPath, IJavaProject project) throws CoreException { + if (AdtConstants.CONTAINER_FRAMEWORK.equals(containerPath.toString())) { + IClasspathContainer container = allocateAndroidContainer(project); + if (container != null) { + JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_FRAMEWORK), + new IJavaProject[] { project }, + new IClasspathContainer[] { container }, + new NullProgressMonitor()); + } + } + } + + /** + * Updates the {@link IJavaProject} objects with new android framework container. This forces + * JDT to recompile them. + * @param androidProjects the projects to update. + * @return <code>true</code> if success, <code>false</code> otherwise. + */ + static boolean updateProjects(IJavaProject[] androidProjects) { + try { + // Allocate a new AndroidClasspathContainer, and associate it to the android framework + // container id for each projects. + // By providing a new association between a container id and a IClasspathContainer, + // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with + // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of + // the projects. + int projectCount = androidProjects.length; + + IClasspathContainer[] containers = new IClasspathContainer[projectCount]; + for (int i = 0 ; i < projectCount; i++) { + containers[i] = allocateAndroidContainer(androidProjects[i]); + } + + // give each project their new container in one call. + JavaCore.setClasspathContainer( + new Path(AdtConstants.CONTAINER_FRAMEWORK), + androidProjects, containers, new NullProgressMonitor()); + + return true; + } catch (JavaModelException e) { + return false; + } + } + + /** + * Allocates and returns an {@link AndroidClasspathContainer} object with the proper + * path to the framework jar file. + * @param javaProject The java project that will receive the container. + */ + private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { + final IProject iProject = javaProject.getProject(); + + String markerMessage = null; + boolean outputToConsole = true; + IAndroidTarget target = null; + + try { + AdtPlugin plugin = AdtPlugin.getDefault(); + if (plugin == null) { // This is totally weird, but I've seen it happen! + return null; + } + + synchronized (Sdk.getLock()) { + boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; + + // check if the project has a valid target. + ProjectState state = Sdk.getProjectState(iProject); + if (state == null) { + // looks like the project state (project.properties) couldn't be read! + markerMessage = String.format( + "Project has no %1$s file! Edit the project properties to set one.", + SdkConstants.FN_PROJECT_PROPERTIES); + } else { + // this might be null if the sdk is not yet loaded. + target = state.getTarget(); + + // if we are loaded and the target is non null, we create a valid + // ClassPathContainer + if (sdkIsLoaded && target != null) { + // check the renderscript support mode. If support mode is enabled, + // target API must be 18+ + if (!state.getRenderScriptSupportMode() || + target.getVersion().getApiLevel() >= 18) { + // first make sure the target has loaded its data + Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/); + + String targetName = target.getClasspathName(); + + return new AndroidClasspathContainer( + createClasspathEntries(iProject, target, targetName), + new Path(AdtConstants.CONTAINER_FRAMEWORK), + targetName, + IClasspathContainer.K_DEFAULT_SYSTEM); + } else { + markerMessage = "Renderscript support mode requires compilation target API to be 18+."; + } + } else { + // In case of error, we'll try different thing to provide the best error message + // possible. + // Get the project's target's hash string (if it exists) + String hashString = state.getTargetHashString(); + + if (hashString == null || hashString.length() == 0) { + // if there is no hash string we only show this if the SDK is loaded. + // For a project opened at start-up with no target, this would be displayed + // twice, once when the project is opened, and once after the SDK has + // finished loading. + // By testing the sdk is loaded, we only show this once in the console. + if (sdkIsLoaded) { + markerMessage = String.format( + "Project has no target set. Edit the project properties to set one."); + } + } else if (sdkIsLoaded) { + markerMessage = String.format( + "Unable to resolve target '%s'", hashString); + } else { + // this is the case where there is a hashString but the SDK is not yet + // loaded and therefore we can't get the target yet. + // We check if there is a cache of the needed information. + AndroidClasspathContainer container = getContainerFromCache(iProject, + target); + + if (container == null) { + // either the cache was wrong (ie folder does not exists anymore), or + // there was no cache. In this case we need to make sure the project + // is resolved again after the SDK is loaded. + plugin.setProjectToResolve(javaProject); + + markerMessage = String.format( + "Unable to resolve target '%s' until the SDK is loaded.", + hashString); + + // let's not log this one to the console as it will happen at + // every boot, and it's expected. (we do keep the error marker though). + outputToConsole = false; + + } else { + // we created a container from the cache, so we register the project + // to be checked for cache validity once the SDK is loaded + plugin.setProjectToCheck(javaProject); + + // and return the container + return container; + } + } + } + } + + // return a dummy container to replace the one we may have had before. + // It'll be replaced by the real when if/when the target is resolved if/when the + // SDK finishes loading. + return new IClasspathContainer() { + @Override + public IClasspathEntry[] getClasspathEntries() { + return new IClasspathEntry[0]; + } + + @Override + public String getDescription() { + return "Unable to get system library for the project"; + } + + @Override + public int getKind() { + return IClasspathContainer.K_DEFAULT_SYSTEM; + } + + @Override + public IPath getPath() { + return null; + } + }; + } + } finally { + processError(iProject, markerMessage, AdtConstants.MARKER_TARGET, outputToConsole); + } + } + + /** + * Creates and returns an array of {@link IClasspathEntry} objects for the android + * framework and optional libraries. + * <p/>This references the OS path to the android.jar and the + * java doc directory. This is dynamically created when a project is opened, + * and never saved in the project itself, so there's no risk of storing an + * obsolete path. + * The method also stores the paths used to create the entries in the project persistent + * properties. A new {@link AndroidClasspathContainer} can be created from the stored path + * using the {@link #getContainerFromCache(IProject)} method. + * @param project + * @param target The target that contains the libraries. + * @param targetName + */ + private static IClasspathEntry[] createClasspathEntries(IProject project, + IAndroidTarget target, String targetName) { + + // get the path from the target + String[] paths = getTargetPaths(target); + + // create the classpath entry from the paths + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target); + + // paths now contains all the path required to recreate the IClasspathEntry with no + // target info. We encode them in a single string, with each path separated by + // OS path separator. + StringBuilder sb = new StringBuilder(CACHE_VERSION); + for (String p : paths) { + sb.append(PATH_SEPARATOR); + sb.append(p); + } + + // store this in a project persistent property + ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); + ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); + + return entries; + } + + /** + * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. + */ + private static AndroidClasspathContainer getContainerFromCache(IProject project, + IAndroidTarget target) { + // get the cached info from the project persistent properties. + String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); + String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); + if (cache == null || targetNameCache == null) { + return null; + } + + // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. + if (cache.startsWith(CACHE_VERSION_SEP) == false) { + return null; + } + + cache = cache.substring(CACHE_VERSION_SEP.length()); + + // the cache contains multiple paths, separated by a character guaranteed to not be in + // the path (\u001C). + // The first 3 are for android.jar (jar, source, doc), the rest are for the optional + // libraries and should contain at least one doc and a jar (if there are any libraries). + // Therefore, the path count should be 3 or 5+ + String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (paths.length < 3 || paths.length == 4) { + return null; + } + + // now we check the paths actually exist. + // There's an exception: If the source folder for android.jar does not exist, this is + // not a problem, so we skip it. + // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a + // bit differently. + try { + if (new File(paths[CACHE_INDEX_JAR]).exists() == false || + new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { + return null; + } + + // check the path for the add-ons, if they exist. + if (paths.length > CACHE_INDEX_ADD_ON_START) { + + // check the docs path separately from the rest of the paths as it's a URI. + if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { + return null; + } + + // now just check the remaining paths. + for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { + String path = paths[i]; + if (path.length() > 0) { + File f = new File(path); + if (f.exists() == false) { + return null; + } + } + } + } + } catch (URISyntaxException e) { + return null; + } + + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target); + + return new AndroidClasspathContainer(entries, + new Path(AdtConstants.CONTAINER_FRAMEWORK), + targetNameCache, IClasspathContainer.K_DEFAULT_SYSTEM); + } + + /** + * Generates an array of {@link IClasspathEntry} from a set of paths. + * @see #getTargetPaths(IAndroidTarget) + */ + private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths, + IAndroidTarget target) { + ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>(); + + // First, we create the IClasspathEntry for the framework. + // now add the android framework to the class path. + // create the path object. + IPath androidLib = new Path(paths[CACHE_INDEX_JAR]); + + IPath androidSrc = null; + String androidSrcOsPath = null; + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + if (target != null) { + androidSrcOsPath = + ProjectHelper.loadStringProperty(root, getAndroidSourceProperty(target)); + } + if (androidSrcOsPath != null && androidSrcOsPath.trim().length() > 0) { + androidSrc = new Path(androidSrcOsPath); + } + if (androidSrc == null) { + androidSrc = new Path(paths[CACHE_INDEX_SRC]); + File androidSrcFile = new File(paths[CACHE_INDEX_SRC]); + if (!androidSrcFile.isDirectory()) { + androidSrc = null; + } + } + + if (androidSrc == null && target != null) { + Bundle bundle = getSourceBundle(); + + if (bundle != null) { + AndroidVersion version = target.getVersion(); + String apiString = version.getApiString(); + String sourcePath = apiString + SOURCES_ZIP; + URL sourceURL = bundle.getEntry(sourcePath); + if (sourceURL != null) { + URL url = null; + try { + url = FileLocator.resolve(sourceURL); + } catch (IOException ignore) { + } + if (url != null) { + androidSrcOsPath = url.getFile(); + if (new File(androidSrcOsPath).isFile()) { + androidSrc = new Path(androidSrcOsPath); + } + } + } + } + } + + // create the java doc link. + String androidApiURL = ProjectHelper.loadStringProperty(root, PROPERTY_ANDROID_API); + String apiURL = null; + if (androidApiURL != null && testURL(androidApiURL)) { + apiURL = androidApiURL; + } else { + if (testURL(paths[CACHE_INDEX_DOCS_URI])) { + apiURL = paths[CACHE_INDEX_DOCS_URI]; + } else if (testURL(ANDROID_API_REFERENCE)) { + apiURL = ANDROID_API_REFERENCE; + } + } + + IClasspathAttribute[] attributes = null; + if (apiURL != null && !NULL_API_URL.equals(apiURL)) { + IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, apiURL); + attributes = new IClasspathAttribute[] { + cpAttribute + }; + } + // create the access rule to restrict access to classes in + // com.android.internal + IAccessRule accessRule = JavaCore.newAccessRule(new Path("com/android/internal/**"), //$NON-NLS-1$ + IAccessRule.K_NON_ACCESSIBLE); + + IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(androidLib, + androidSrc, // source attachment path + null, // default source attachment root path. + new IAccessRule[] { accessRule }, + attributes, + false // not exported. + ); + + list.add(frameworkClasspathEntry); + + // now deal with optional libraries + if (paths.length >= 5) { + String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; + int i = 4; + while (i < paths.length) { + Path jarPath = new Path(paths[i++]); + + attributes = null; + if (docPath.length() > 0) { + attributes = new IClasspathAttribute[] { + JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, docPath) + }; + } + + IClasspathEntry entry = JavaCore.newLibraryEntry( + jarPath, + null, // source attachment path + null, // default source attachment root path. + null, + attributes, + false // not exported. + ); + list.add(entry); + } + } + + if (apiURL != null) { + ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, apiURL); + } + if (androidSrc != null && target != null) { + ProjectHelper.saveStringProperty(root, getAndroidSourceProperty(target), + androidSrc.toOSString()); + } + return list.toArray(new IClasspathEntry[list.size()]); + } + + private static Bundle getSourceBundle() { + String bundleId = System.getProperty(COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE, + COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE); + Bundle bundle = Platform.getBundle(bundleId); + return bundle; + } + + private static String getAndroidSourceProperty(IAndroidTarget target) { + if (target == null) { + return null; + } + String androidSourceProperty = PROPERTY_ANDROID_SOURCE + "_" + + target.getVersion().getApiString(); + return androidSourceProperty; + } + + /** + * Cache results for testURL: Some are expensive to compute, and this is + * called repeatedly (perhaps for each open project) + */ + private static final Map<String, Boolean> sRecentUrlValidCache = + Maps.newHashMapWithExpectedSize(4); + + @SuppressWarnings("resource") // Eclipse does not handle Closeables#closeQuietly + private static boolean testURL(String androidApiURL) { + Boolean cached = sRecentUrlValidCache.get(androidApiURL); + if (cached != null) { + return cached.booleanValue(); + } + boolean valid = false; + InputStream is = null; + try { + URL testURL = new URL(androidApiURL); + URLConnection connection = testURL.openConnection(); + // Only try for 5 seconds (though some implementations ignore this flag) + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + is = connection.getInputStream(); + valid = true; + } catch (Exception ignore) { + } finally { + Closeables.closeQuietly(is); + } + + sRecentUrlValidCache.put(androidApiURL, valid); + + return valid; + } + + /** + * Checks the projects' caches. If the cache was valid, the project is removed from the list. + * @param projects the list of projects to check. + */ + public static void checkProjectsCache(ArrayList<IJavaProject> projects) { + Sdk currentSdk = Sdk.getCurrent(); + int i = 0; + projectLoop: while (i < projects.size()) { + IJavaProject javaProject = projects.get(i); + IProject iProject = javaProject.getProject(); + + // check if the project is opened + if (iProject.isOpen() == false) { + // remove from the list + // we do not increment i in this case. + projects.remove(i); + + continue; + } + + // project that have been resolved before the sdk was loaded + // will have a ProjectState where the IAndroidTarget is null + // so we load the target now that the SDK is loaded. + IAndroidTarget target = currentSdk.loadTargetAndBuildTools( + Sdk.getProjectState(iProject)); + if (target == null) { + // this is really not supposed to happen. This would mean there are cached paths, + // but project.properties was deleted. Keep the project in the list to force + // a resolve which will display the error. + i++; + continue; + } + + String[] targetPaths = getTargetPaths(target); + + // now get the cached paths + String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); + if (cache == null) { + // this should not happen. We'll force resolve again anyway. + i++; + continue; + } + + String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (cachedPaths.length < 3 || cachedPaths.length == 4) { + // paths length is wrong. simply resolve the project again + i++; + continue; + } + + // Now we compare the paths. The first 4 can be compared directly. + // because of case sensitiveness we need to use File objects + + if (targetPaths.length != cachedPaths.length) { + // different paths, force resolve again. + i++; + continue; + } + + // compare the main paths (android.jar, main sources, main javadoc) + if (new File(targetPaths[CACHE_INDEX_JAR]).equals( + new File(cachedPaths[CACHE_INDEX_JAR])) == false || + new File(targetPaths[CACHE_INDEX_SRC]).equals( + new File(cachedPaths[CACHE_INDEX_SRC])) == false || + new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( + new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { + // compare optional libraries javadoc + if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( + new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + // testing the optional jar files is a little bit trickier. + // The order is not guaranteed to be identical. + // From a previous test, we do know however that there is the same number. + // The number of libraries should be low enough that we can simply go through the + // lists manually. + targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { + String targetPath = targetPaths[tpi]; + + // look for a match in the other array + for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { + if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { + // found a match. Try the next targetPath + continue targetLoop; + } + } + + // if we stop here, we haven't found a match, which means there's a + // discrepancy in the libraries. We force a resolve. + i++; + continue projectLoop; + } + } + + // at the point the check passes, and we can remove the project from the list. + // we do not increment i in this case. + projects.remove(i); + } + } + + /** + * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. + * <p/>The paths are always in the same order. + * <ul> + * <li>Path to android.jar</li> + * <li>Path to the source code for android.jar</li> + * <li>Path to the javadoc for the android platform</li> + * </ul> + * Additionally, if there are optional libraries, the array will contain: + * <ul> + * <li>Path to the libraries javadoc</li> + * <li>Path to the first .jar file</li> + * <li>(more .jar as needed)</li> + * </ul> + */ + private static String[] getTargetPaths(IAndroidTarget target) { + ArrayList<String> paths = new ArrayList<String>(); + + // first, we get the path for android.jar + // The order is: android.jar, source folder, docs folder + paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + paths.add(target.getPath(IAndroidTarget.SOURCES)); + paths.add(AdtPlugin.getUrlDoc()); + + // now deal with optional libraries. + IOptionalLibrary[] libraries = target.getOptionalLibraries(); + if (libraries != null) { + // all the optional libraries use the same javadoc, so we start with this + String targetDocPath = target.getPath(IAndroidTarget.DOCS); + if (targetDocPath != null) { + paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); + } else { + // we add an empty string, to always have the same count. + paths.add(""); + } + + // because different libraries could use the same jar file, we make sure we add + // each jar file only once. + HashSet<String> visitedJars = new HashSet<String>(); + for (IOptionalLibrary library : libraries) { + String jarPath = library.getJarPath(); + if (visitedJars.contains(jarPath) == false) { + visitedJars.add(jarPath); + paths.add(jarPath); + } + } + } + + return paths.toArray(new String[paths.size()]); + } + + @Override + public boolean canUpdateClasspathContainer(IPath containerPath, IJavaProject project) { + return true; + } + + @Override + public void requestClasspathContainerUpdate(IPath containerPath, IJavaProject project, + IClasspathContainer containerSuggestion) throws CoreException { + AdtPlugin plugin = AdtPlugin.getDefault(); + + synchronized (Sdk.getLock()) { + boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; + + // check if the project has a valid target. + IAndroidTarget target = null; + if (sdkIsLoaded) { + target = Sdk.getCurrent().getTarget(project.getProject()); + } + if (sdkIsLoaded && target != null) { + String[] paths = getTargetPaths(target); + IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); + IClasspathEntry[] entries = containerSuggestion.getClasspathEntries(); + for (int i = 0; i < entries.length; i++) { + IClasspathEntry entry = entries[i]; + if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { + IPath entryPath = entry.getPath(); + + if (entryPath != null) { + if (entryPath.equals(android_lib)) { + IPath entrySrcPath = entry.getSourceAttachmentPath(); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + if (entrySrcPath != null) { + ProjectHelper.saveStringProperty(root, + getAndroidSourceProperty(target), + entrySrcPath.toString()); + } else { + ProjectHelper.saveStringProperty(root, + getAndroidSourceProperty(target), null); + } + IClasspathAttribute[] extraAttributtes = entry.getExtraAttributes(); + if (extraAttributtes.length == 0) { + ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, + NULL_API_URL); + } + for (int j = 0; j < extraAttributtes.length; j++) { + IClasspathAttribute extraAttribute = extraAttributtes[j]; + String value = extraAttribute.getValue(); + if ((value == null || value.trim().length() == 0) + && IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME + .equals(extraAttribute.getName())) { + value = NULL_API_URL; + } + if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME + .equals(extraAttribute.getName())) { + ProjectHelper.saveStringProperty(root, + PROPERTY_ANDROID_API, value); + + } + } + } + } + } + } + rebindClasspathEntries(project.getJavaModel(), containerPath); + } + } + } + + private static void rebindClasspathEntries(IJavaModel model, IPath containerPath) + throws JavaModelException { + ArrayList<IJavaProject> affectedProjects = new ArrayList<IJavaProject>(); + + IJavaProject[] projects = model.getJavaProjects(); + for (int i = 0; i < projects.length; i++) { + IJavaProject project = projects[i]; + IClasspathEntry[] entries = project.getRawClasspath(); + for (int k = 0; k < entries.length; k++) { + IClasspathEntry curr = entries[k]; + if (curr.getEntryKind() == IClasspathEntry.CPE_CONTAINER + && containerPath.equals(curr.getPath())) { + affectedProjects.add(project); + } + } + } + if (!affectedProjects.isEmpty()) { + IJavaProject[] affected = affectedProjects + .toArray(new IJavaProject[affectedProjects.size()]); + updateProjects(affected); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerPage.java new file mode 100644 index 000000000..b02765012 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerPage.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2010 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.project; + +import com.android.ide.eclipse.adt.AdtConstants; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.internal.ui.dialogs.StatusInfo; +import org.eclipse.jdt.internal.ui.dialogs.StatusUtil; +import org.eclipse.jdt.ui.wizards.IClasspathContainerPage; +import org.eclipse.jdt.ui.wizards.IClasspathContainerPageExtension; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; + +import java.util.Arrays; + +public class AndroidClasspathContainerPage extends WizardPage implements IClasspathContainerPage, + IClasspathContainerPageExtension { + + private IProject mOwnerProject; + + private String mLibsProjectName; + + private Combo mProjectsCombo; + + private IStatus mCurrStatus; + + private boolean mPageVisible; + + public AndroidClasspathContainerPage() { + super("AndroidClasspathContainerPage"); //$NON-NLS-1$ + mPageVisible = false; + mCurrStatus = new StatusInfo(); + setTitle("Android Libraries"); + setDescription("This container manages classpath entries for Android container"); + } + + @Override + public IClasspathEntry getSelection() { + IPath path = new Path(AdtConstants.CONTAINER_FRAMEWORK); + + final int index = this.mProjectsCombo.getSelectionIndex(); + if (index != -1) { + final String selectedProjectName = this.mProjectsCombo.getItem(index); + + if (this.mOwnerProject == null + || !selectedProjectName.equals(this.mOwnerProject.getName())) { + path = path.append(selectedProjectName); + } + } + + return JavaCore.newContainerEntry(path); + } + + @Override + public void setSelection(final IClasspathEntry cpentry) { + final IPath path = cpentry == null ? null : cpentry.getPath(); + + if (path == null || path.segmentCount() == 1) { + if (this.mOwnerProject != null) { + this.mLibsProjectName = this.mOwnerProject.getName(); + } + } else { + this.mLibsProjectName = path.segment(1); + } + } + + @Override + public void createControl(final Composite parent) { + final Composite composite = new Composite(parent, SWT.NONE); + composite.setLayout(new GridLayout(2, false)); + + final Label label = new Label(composite, SWT.NONE); + label.setText("Project:"); + + final String[] androidProjects = getAndroidProjects(); + + this.mProjectsCombo = new Combo(composite, SWT.READ_ONLY); + this.mProjectsCombo.setItems(androidProjects); + + final int index; + + if (this.mOwnerProject != null) { + index = indexOf(androidProjects, this.mLibsProjectName); + } else { + if (this.mProjectsCombo.getItemCount() > 0) { + index = 0; + } else { + index = -1; + } + } + + if (index != -1) { + this.mProjectsCombo.select(index); + } + + final GridData gd = new GridData(); + gd.grabExcessHorizontalSpace = true; + gd.minimumWidth = 100; + + this.mProjectsCombo.setLayoutData(gd); + + setControl(composite); + } + + @Override + public boolean finish() { + return true; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + mPageVisible = visible; + // policy: wizards are not allowed to come up with an error message + if (visible && mCurrStatus.matches(IStatus.ERROR)) { + StatusInfo status = new StatusInfo(); + status.setError(""); //$NON-NLS-1$ + mCurrStatus = status; + } + updateStatus(mCurrStatus); + } + + /** + * Updates the status line and the OK button according to the given status + * + * @param status status to apply + */ + protected void updateStatus(IStatus status) { + mCurrStatus = status; + setPageComplete(!status.matches(IStatus.ERROR)); + if (mPageVisible) { + StatusUtil.applyToStatusLine(this, status); + } + } + + /** + * Updates the status line and the OK button according to the status + * evaluate from an array of status. The most severe error is taken. In case + * that two status with the same severity exists, the status with lower + * index is taken. + * + * @param status the array of status + */ + protected void updateStatus(IStatus[] status) { + updateStatus(StatusUtil.getMostSevere(status)); + } + + @Override + public void initialize(final IJavaProject project, final IClasspathEntry[] currentEntries) { + this.mOwnerProject = (project == null ? null : project.getProject()); + } + + private static String[] getAndroidProjects() { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + final String[] names = new String[projects.length]; + for (int i = 0; i < projects.length; i++) { + names[i] = projects[i].getName(); + } + Arrays.sort(names); + return names; + } + + private static int indexOf(final String[] array, final String str) { + for (int i = 0; i < array.length; i++) { + if (array[i].equals(str)) { + return i; + } + } + return -1; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidExportNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidExportNature.java new file mode 100644 index 000000000..218cffe5e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidExportNature.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 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.project; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectNature; +import org.eclipse.core.runtime.CoreException; + +/** + * Project nature for the Android Export Projects. + */ +public class AndroidExportNature implements IProjectNature { + + /** the project this nature object is associated with */ + private IProject mProject; + + /** + * Configures this nature for its project. This is called by the workspace + * when natures are added to the project using + * <code>IProject.setDescription</code> and should not be called directly + * by clients. The nature extension id is added to the list of natures + * before this method is called, and need not be added here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * <code>IProject.setDescription</code>, but the nature will remain in + * the project description. + * + * @see org.eclipse.core.resources.IProjectNature#configure() + * @throws CoreException if configuration fails. + */ + @Override + public void configure() throws CoreException { + // nothing to do. + } + + /** + * De-configures this nature for its project. This is called by the + * workspace when natures are removed from the project using + * <code>IProject.setDescription</code> and should not be called directly + * by clients. The nature extension id is removed from the list of natures + * before this method is called, and need not be removed here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * <code>IProject.setDescription</code>, but the nature will still be + * removed from the project description. + * + * The Android nature removes the custom pre builder and APK builder. + * + * @see org.eclipse.core.resources.IProjectNature#deconfigure() + * @throws CoreException if configuration fails. + */ + @Override + public void deconfigure() throws CoreException { + // nothing to do + } + + /** + * Returns the project to which this project nature applies. + * + * @return the project handle + * @see org.eclipse.core.resources.IProjectNature#getProject() + */ + @Override + public IProject getProject() { + return mProject; + } + + /** + * Sets the project to which this nature applies. Used when instantiating + * this project nature runtime. This is called by + * <code>IProject.create()</code> or + * <code>IProject.setDescription()</code> and should not be called + * directly by clients. + * + * @param project the project to which this nature applies + * @see org.eclipse.core.resources.IProjectNature#setProject(org.eclipse.core.resources.IProject) + */ + @Override + public void setProject(IProject project) { + mProject = project; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidManifestHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidManifestHelper.java new file mode 100644 index 000000000..eaa309668 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidManifestHelper.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.ide.common.xml.AndroidManifestParser; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.io.FileWrapper; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IJavaProject; +import org.xml.sax.SAXException; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; + +public class AndroidManifestHelper { + + /** + * Parses the Android Manifest, and returns an object containing the result of the parsing. + * <p/> + * This method can also gather XML error during the parsing. This is done by using an + * {@link XmlErrorHandler} to mark the files in case of error, as well as a given + * {@link XmlErrorListener}. To use a different error handler, consider using + * {@link AndroidManifestParser#parse(IAbstractFile, boolean, com.android.sdklib.xml.AndroidManifestParser.ManifestErrorHandler)} + * directly. + * + * @param manifestFile the {@link IFile} representing the manifest file. + * @param gatherData indicates whether the parsing will extract data from the manifest. If null, + * the method will always return null. + * @param errorListener an optional error listener. If non null, then the parser will also + * look for XML errors. + * @return an {@link ManifestData} or null if the parsing failed. + * @throws ParserConfigurationException + * @throws StreamException + * @throws IOException + * @throws SAXException + */ + public static ManifestData parseUnchecked( + IAbstractFile manifestFile, + boolean gatherData, + XmlErrorListener errorListener) throws SAXException, IOException, + StreamException, ParserConfigurationException { + if (manifestFile != null) { + IFile eclipseFile = null; + if (manifestFile instanceof IFileWrapper) { + eclipseFile = ((IFileWrapper)manifestFile).getIFile(); + } + XmlErrorHandler errorHandler = null; + if (errorListener != null) { + errorHandler = new XmlErrorHandler(eclipseFile, errorListener); + } + + return AndroidManifestParser.parse(manifestFile, gatherData, errorHandler); + } + + return null; + } + + /** + * Parses the Android Manifest, and returns an object containing the result of the parsing. + * <p/> + * This method can also gather XML error during the parsing. This is done by using an + * {@link XmlErrorHandler} to mark the files in case of error, as well as a given + * {@link XmlErrorListener}. To use a different error handler, consider using + * {@link AndroidManifestParser#parse(IAbstractFile, boolean, com.android.sdklib.xml.AndroidManifestParser.ManifestErrorHandler)} + * directly. + * + * @param manifestFile the {@link IFile} representing the manifest file. + * @param gatherData indicates whether the parsing will extract data from the manifest. If null, + * the method will always return null. + * @param errorListener an optional error listener. If non null, then the parser will also + * look for XML errors. + * @return an {@link ManifestData} or null if the parsing failed. + */ + public static ManifestData parse( + IAbstractFile manifestFile, + boolean gatherData, + XmlErrorListener errorListener) { + try { + return parseUnchecked(manifestFile, gatherData, errorListener); + } catch (ParserConfigurationException e) { + AdtPlugin.logAndPrintError(e, AndroidManifestHelper.class.getCanonicalName(), + "Bad parser configuration for %s: %s", + manifestFile.getOsLocation(), + e.getMessage()); + } catch (SAXException e) { + AdtPlugin.logAndPrintError(e, AndroidManifestHelper.class.getCanonicalName(), + "Parser exception for %s: %s", + manifestFile.getOsLocation(), + e.getMessage()); + } catch (IOException e) { + // Don't log a console error when failing to read a non-existing file + if (!(e instanceof FileNotFoundException)) { + AdtPlugin.logAndPrintError(e, AndroidManifestHelper.class.getCanonicalName(), + "I/O error for %s: %s", + manifestFile.getOsLocation(), + e.getMessage()); + } + } catch (StreamException e) { + AdtPlugin.logAndPrintError(e, AndroidManifestHelper.class.getCanonicalName(), + "Unable to read %s: %s", + manifestFile.getOsLocation(), + e.getMessage()); + } + + return null; + } + + /** + * Parses the Android Manifest for a given project, and returns an object containing + * the result of the parsing. + * <p/> + * This method can also gather XML error during the parsing. This is done by using an + * {@link XmlErrorHandler} to mark the files in case of error, as well as a given + * {@link XmlErrorListener}. To use a different error handler, consider using + * {@link AndroidManifestParser#parse(IAbstractFile, boolean, com.android.sdklib.xml.AndroidManifestParser.ManifestErrorHandler)} + * directly. + * + * @param javaProject the project containing the manifest to parse. + * @param gatherData indicates whether the parsing will extract data from the manifest. If null, + * the method will always return null. + * @param errorListener an optional error listener. If non null, then the parser will also + * look for XML errors. + * @return an {@link ManifestData} or null if the parsing failed. + */ + public static ManifestData parse( + IJavaProject javaProject, + boolean gatherData, + XmlErrorListener errorListener) { + + IFile manifestFile = ProjectHelper.getManifest(javaProject.getProject()); + if (manifestFile != null) { + return parse(new IFileWrapper(manifestFile), gatherData, errorListener); + } + + return null; + } + + /** + * Parses the manifest file only for error check. + * @param manifestFile The manifest file to parse. + * @param errorListener the {@link XmlErrorListener} object being notified of the presence + * of errors. + */ + public static void parseForError(IFile manifestFile, XmlErrorListener errorListener) { + parse(new IFileWrapper(manifestFile), false, errorListener); + } + + /** + * Parses the manifest file, and collects data. + * @param manifestFile The manifest file to parse. + * @return an {@link ManifestData} or null if the parsing failed. + */ + public static ManifestData parseForData(IFile manifestFile) { + return parse(new IFileWrapper(manifestFile), true, null); + } + + /** + * Parses the manifest file, and collects data. + * @param project the project containing the manifest. + * @return an {@link AndroidManifestHelper} or null if the parsing failed. + */ + public static ManifestData parseForData(IProject project) { + IFile manifestFile = ProjectHelper.getManifest(project); + if (manifestFile != null) { + return parse(new IFileWrapper(manifestFile), true, null); + } + + return null; + } + + /** + * Parses the manifest file, and collects data. + * + * @param osManifestFilePath The OS path of the manifest file to parse. + * @return an {@link AndroidManifestHelper} or null if the parsing failed. + */ + public static ManifestData parseForData(String osManifestFilePath) { + return parse(new FileWrapper(osManifestFilePath), true, null); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java new file mode 100644 index 000000000..3b1c29fe9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.build.builders.PostCompilerBuilder; +import com.android.ide.eclipse.adt.internal.build.builders.PreCompilerBuilder; +import com.android.ide.eclipse.adt.internal.build.builders.ResourceManagerBuilder; + +import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IProjectNature; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.JavaCore; + +/** + * Project nature for the Android Projects. + */ +public class AndroidNature implements IProjectNature { + + /** the project this nature object is associated with */ + private IProject mProject; + + /** + * Configures this nature for its project. This is called by the workspace + * when natures are added to the project using + * <code>IProject.setDescription</code> and should not be called directly + * by clients. The nature extension id is added to the list of natures + * before this method is called, and need not be added here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * <code>IProject.setDescription</code>, but the nature will remain in + * the project description. + * + * The Android nature adds the pre-builder and the APK builder if necessary. + * + * @see org.eclipse.core.resources.IProjectNature#configure() + * @throws CoreException if configuration fails. + */ + @Override + public void configure() throws CoreException { + configureResourceManagerBuilder(mProject); + configurePreBuilder(mProject); + configureApkBuilder(mProject); + } + + /** + * De-configures this nature for its project. This is called by the + * workspace when natures are removed from the project using + * <code>IProject.setDescription</code> and should not be called directly + * by clients. The nature extension id is removed from the list of natures + * before this method is called, and need not be removed here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * <code>IProject.setDescription</code>, but the nature will still be + * removed from the project description. + * + * The Android nature removes the custom pre builder and APK builder. + * + * @see org.eclipse.core.resources.IProjectNature#deconfigure() + * @throws CoreException if configuration fails. + */ + @Override + public void deconfigure() throws CoreException { + // remove the android builders + removeBuilder(mProject, ResourceManagerBuilder.ID); + removeBuilder(mProject, PreCompilerBuilder.ID); + removeBuilder(mProject, PostCompilerBuilder.ID); + } + + /** + * Returns the project to which this project nature applies. + * + * @return the project handle + * @see org.eclipse.core.resources.IProjectNature#getProject() + */ + @Override + public IProject getProject() { + return mProject; + } + + /** + * Sets the project to which this nature applies. Used when instantiating + * this project nature runtime. This is called by + * <code>IProject.create()</code> or + * <code>IProject.setDescription()</code> and should not be called + * directly by clients. + * + * @param project the project to which this nature applies + * @see org.eclipse.core.resources.IProjectNature#setProject(org.eclipse.core.resources.IProject) + */ + @Override + public void setProject(IProject project) { + mProject = project; + } + + /** + * Adds the Android Nature and the Java Nature to the project if it doesn't + * already have them. + * + * @param project An existing or new project to update + * @param monitor An optional progress monitor. Can be null. + * @param addAndroidNature true if the Android Nature should be added to the project; false to + * add only the Java nature. + * @throws CoreException if fails to change the nature. + */ + public static synchronized void setupProjectNatures(IProject project, + IProgressMonitor monitor, boolean addAndroidNature) throws CoreException { + if (project == null || !project.isOpen()) return; + if (monitor == null) monitor = new NullProgressMonitor(); + + // Add the natures. We need to add the Java nature first, so it adds its builder to the + // project first. This way, when the android nature is added, we can control where to put + // the android builders in relation to the java builder. + // Adding the java nature after the android one, would place the java builder before the + // android builders. + addNatureToProjectDescription(project, JavaCore.NATURE_ID, monitor); + if (addAndroidNature) { + addNatureToProjectDescription(project, AdtConstants.NATURE_DEFAULT, monitor); + } + } + + /** + * Add the specified nature to the specified project. The nature is only + * added if not already present. + * <p/> + * Android Natures are always inserted at the beginning of the list of natures in order to + * have the jdt views/dialogs display the proper icon. + * + * @param project The project to modify. + * @param natureId The Id of the nature to add. + * @param monitor An existing progress monitor. + * @throws CoreException if fails to change the nature. + */ + private static void addNatureToProjectDescription(IProject project, + String natureId, IProgressMonitor monitor) throws CoreException { + if (!project.hasNature(natureId)) { + + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + String[] newNatures = new String[natures.length + 1]; + + // Android natures always come first. + if (natureId.equals(AdtConstants.NATURE_DEFAULT)) { + System.arraycopy(natures, 0, newNatures, 1, natures.length); + newNatures[0] = natureId; + } else { + System.arraycopy(natures, 0, newNatures, 0, natures.length); + newNatures[natures.length] = natureId; + } + + description.setNatureIds(newNatures); + project.setDescription(description, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the ResourceManagerBuilder, if its not already there. It'll insert + * itself as the first builder. + * @throws CoreException + * + */ + public static void configureResourceManagerBuilder(IProject project) + throws CoreException { + // get the builder list + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + // look for the builder in case it's already there. + for (int i = 0; i < commands.length; ++i) { + if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + // it's not there, lets add it at the beginning of the builders + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 1, commands.length); + ICommand command = desc.newCommand(); + command.setBuilderName(ResourceManagerBuilder.ID); + newCommands[0] = command; + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + /** + * Adds the PreCompilerBuilder if its not already there. It'll check for + * presence of the ResourceManager and insert itself right after. + * @param project + * @throws CoreException + */ + public static void configurePreBuilder(IProject project) + throws CoreException { + // get the builder list + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + // look for the builder in case it's already there. + for (int i = 0; i < commands.length; ++i) { + if (PreCompilerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + // we need to add it after the resource manager builder. + // Let's look for it + int index = -1; + for (int i = 0; i < commands.length; ++i) { + if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { + index = i; + break; + } + } + + // we're inserting after + index++; + + // do the insertion + + // copy the builders before. + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 0, index); + + // insert the new builder + ICommand command = desc.newCommand(); + command.setBuilderName(PreCompilerBuilder.ID); + newCommands[index] = command; + + // copy the builder after + System.arraycopy(commands, index, newCommands, index + 1, commands.length-index); + + // set the new builders in the project + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + public static void configureApkBuilder(IProject project) + throws CoreException { + // Add the .apk builder at the end if it's not already there + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + for (int i = 0; i < commands.length; ++i) { + if (PostCompilerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 0, commands.length); + ICommand command = desc.newCommand(); + command.setBuilderName(PostCompilerBuilder.ID); + newCommands[commands.length] = command; + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + /** + * Removes a builder from the project. + * @param project The project to remove the builder from. + * @param id The String ID of the builder to remove. + * @return true if the builder was found and removed. + * @throws CoreException + */ + public static boolean removeBuilder(IProject project, String id) throws CoreException { + IProjectDescription description = project.getDescription(); + ICommand[] commands = description.getBuildSpec(); + for (int i = 0; i < commands.length; ++i) { + if (id.equals(commands[i].getBuilderName())) { + ICommand[] newCommands = new ICommand[commands.length - 1]; + System.arraycopy(commands, 0, newCommands, 0, i); + System.arraycopy(commands, i + 1, newCommands, i, commands.length - i - 1); + description.setBuildSpec(newCommands); + project.setDescription(description, null); + return true; + } + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java new file mode 100644 index 000000000..903914684 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2009 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.project; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IPath; + +import java.util.HashSet; +import java.util.Iterator; + +/** + * Registers which apk was installed on which device. + * <p/> + * The goal of this class is to remember the installation of APKs on devices, and provide + * information about whether a new APK should be installed on a device prior to running the + * application from a launch configuration. + * <p/> + * The manager uses {@link IProject} and {@link IDevice} to identify the target device and the + * (project generating the) APK. This ensures that disconnected and reconnected devices will + * always receive new APKs (since the version may not match). + * <p/> + * This is a singleton. To get the instance, use {@link #getInstance()} + */ +public final class ApkInstallManager { + + private final static ApkInstallManager sThis = new ApkInstallManager(); + + /** + * Internal struct to associate a project and a device. + */ + private final static class ApkInstall { + public ApkInstall(IProject project, String packageName, IDevice device) { + this.project = project; + this.packageName = packageName; + this.device = device; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ApkInstall) { + ApkInstall apkObj = (ApkInstall)obj; + + return (device == apkObj.device && project.equals(apkObj.project) && + packageName.equals(apkObj.packageName)); + } + + return false; + } + + @Override + public int hashCode() { + return (device.getSerialNumber() + project.getName() + packageName).hashCode(); + } + + final IProject project; + final String packageName; + final IDevice device; + } + + /** + * Receiver and parser for the "pm path package" command. + */ + private final static class PmReceiver extends MultiLineReceiver { + boolean foundPackage = false; + @Override + public void processNewLines(String[] lines) { + // if the package if found, then pm will show a line starting with "package:/" + if (foundPackage == false) { // just in case this is called several times for multilines + for (String line : lines) { + if (line.startsWith("package:/")) { + foundPackage = true; + break; + } + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + } + + /** + * Hashset of the list of installed package. Hashset used to ensure we don't re-add new + * objects for the same app. + */ + private final HashSet<ApkInstall> mInstallList = new HashSet<ApkInstall>(); + + public static ApkInstallManager getInstance() { + return sThis; + } + + /** + * Registers an installation of <var>project</var> onto <var>device</var> + * @param project The project that was installed. + * @param packageName the package name of the apk + * @param device The device that received the installation. + */ + public void registerInstallation(IProject project, String packageName, IDevice device) { + synchronized (mInstallList) { + mInstallList.add(new ApkInstall(project, packageName, device)); + } + } + + /** + * Returns whether a <var>project</var> was installed on the <var>device</var>. + * @param project the project that may have been installed. + * @param device the device that may have received the installation. + * @return + */ + public boolean isApplicationInstalled(IProject project, String packageName, IDevice device) { + synchronized (mInstallList) { + ApkInstall found = null; + for (ApkInstall install : mInstallList) { + if (project.equals(install.project) && packageName.equals(install.packageName) && + device == install.device) { + found = install; + break; + } + } + + // check the app is still installed. + if (found != null) { + try { + PmReceiver receiver = new PmReceiver(); + found.device.executeShellCommand("pm path " + packageName, receiver); + if (receiver.foundPackage == false) { + mInstallList.remove(found); + } + + return receiver.foundPackage; + } catch (Exception e) { + // failed to query pm? force reinstall. + return false; + } + } + } + return false; + } + + /** + * Resets registered installations for a specific {@link IProject}. + * <p/>This ensures that {@link #isApplicationInstalled(IProject, IDevice)} will always return + * <code>null</code> for this specified project, for any device. + * @param project the project for which to reset all installations. + */ + public void resetInstallationFor(IProject project) { + synchronized (mInstallList) { + Iterator<ApkInstall> iterator = mInstallList.iterator(); + while (iterator.hasNext()) { + ApkInstall install = iterator.next(); + if (install.project.equals(project)) { + iterator.remove(); + } + } + } + } + + private ApkInstallManager() { + AndroidDebugBridge.addDeviceChangeListener(mDeviceChangeListener); + AndroidDebugBridge.addDebugBridgeChangeListener(mDebugBridgeListener); + GlobalProjectMonitor.getMonitor().addProjectListener(mProjectListener); + } + + private IDebugBridgeChangeListener mDebugBridgeListener = new IDebugBridgeChangeListener() { + /** + * Responds to a bridge change by clearing the full installation list. + * + * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge) + */ + @Override + public void bridgeChanged(AndroidDebugBridge bridge) { + // the bridge changed, there is no way to know which IDevice will be which. + // We reset everything + synchronized (mInstallList) { + mInstallList.clear(); + } + } + }; + + private IDeviceChangeListener mDeviceChangeListener = new IDeviceChangeListener() { + /** + * Responds to a device being disconnected by removing all installations related + * to this device. + * + * @see IDeviceChangeListener#deviceDisconnected(IDevice) + */ + @Override + public void deviceDisconnected(IDevice device) { + synchronized (mInstallList) { + Iterator<ApkInstall> iterator = mInstallList.iterator(); + while (iterator.hasNext()) { + ApkInstall install = iterator.next(); + if (install.device == device) { + iterator.remove(); + } + } + } + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + // nothing to do. + } + + @Override + public void deviceConnected(IDevice device) { + // nothing to do. + } + }; + + private IProjectListener mProjectListener = new IProjectListener() { + /** + * Responds to a closed project by resetting all its installation. + * + * @see IProjectListener#projectClosed(IProject) + */ + @Override + public void projectClosed(IProject project) { + resetInstallationFor(project); + } + + /** + * Responds to a deleted project by resetting all its installation. + * + * @see IProjectListener#projectDeleted(IProject) + */ + @Override + public void projectDeleted(IProject project) { + resetInstallationFor(project); + } + + @Override + public void projectOpened(IProject project) { + // nothing to do. + } + + @Override + public void projectOpenedWithWorkspace(IProject project) { + // nothing to do. + } + + @Override + public void allProjectsOpenedWithWorkspace() { + // nothing to do. + } + + @Override + public void projectRenamed(IProject project, IPath from) { + // project renaming also triggers delete/open events so + // there's nothing to do here (since delete will remove + // whatever's linked to the project from the list). + } + }; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseClasspathContainerInitializer.java new file mode 100644 index 000000000..a58f27d61 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseClasspathContainerInitializer.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2012 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.project; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +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.ClasspathContainerInitializer; + +/** + * Base CPC initializer providing support to all our initializer. + * + */ +abstract class BaseClasspathContainerInitializer extends ClasspathContainerInitializer { + + + /** + * Adds an error to a project, or remove all markers if error message is null + * @param project the project to modify + * @param errorMessage the errorMessage or null to remove errors. + * @param markerType the marker type to be used. + * @param outputToConsole whether to output to the console. + */ + protected static void processError(final IProject project, final String errorMessage, + final String markerType, boolean outputToConsole) { + if (errorMessage != null) { + // log the error and put the marker on the project if we can. + if (outputToConsole) { + AdtPlugin.printErrorToConsole(project, errorMessage); + } + + // Use a job to prevent requiring a workspace lock in this thread. + final String fmessage = errorMessage; + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + BaseProjectHelper.markProject(project, + markerType, + fmessage, 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(); + } else { + // Use a job to prevent requiring a workspace lock in this thread. + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (project.isAccessible()) { + project.deleteMarkers(markerType, true, + IResource.DEPTH_INFINITE); + } + } catch (CoreException e2) { + AdtPlugin.log(e2, null); + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); + markerJob.schedule(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseProjectHelper.java new file mode 100644 index 000000000..57632ea87 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseProjectHelper.java @@ -0,0 +1,527 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.google.common.collect.Lists; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods to manipulate projects. + */ +public final class BaseProjectHelper { + + public static final String TEST_CLASS_OK = null; + + /** + * Project filter to be used with {@link BaseProjectHelper#getAndroidProjects(IProjectFilter)}. + */ + public static interface IProjectFilter { + boolean accept(IProject project); + } + + /** + * returns a list of source classpath for a specified project + * @param javaProject + * @return a list of path relative to the workspace root. + */ + @NonNull + public static List<IPath> getSourceClasspaths(IJavaProject javaProject) { + List<IPath> sourceList = Lists.newArrayList(); + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourceList.add(e.getPath()); + } + } + } + + return sourceList; + } + + /** + * returns a list of source classpath for a specified project + * @param project + * @return a list of path relative to the workspace root. + */ + public static List<IPath> getSourceClasspaths(IProject project) { + IJavaProject javaProject = JavaCore.create(project); + return getSourceClasspaths(javaProject); + } + + /** + * Adds a marker to a file on a specific line. This methods catches thrown + * {@link CoreException}, and returns null instead. + * @param resource the resource to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param lineNumber the line number where to put the mark. If line is < 1, it puts the marker + * on line 1, + * @param severity the severity of the marker. + * @return the IMarker that was added or null if it failed to add one. + */ + public final static IMarker markResource(IResource resource, String markerId, + String message, int lineNumber, int severity) { + return markResource(resource, markerId, message, lineNumber, -1, -1, severity); + } + + /** + * Adds a marker to a file on a specific line, for a specific range of text. This + * methods catches thrown {@link CoreException}, and returns null instead. + * + * @param resource the resource to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param lineNumber the line number where to put the mark. If line is < 1, it puts + * the marker on line 1, + * @param startOffset the beginning offset of the marker (relative to the beginning of + * the document, not the line), or -1 for no range + * @param endOffset the ending offset of the marker + * @param severity the severity of the marker. + * @return the IMarker that was added or null if it failed to add one. + */ + @Nullable + public final static IMarker markResource(IResource resource, String markerId, + String message, int lineNumber, int startOffset, int endOffset, int severity) { + if (!resource.isAccessible()) { + return null; + } + + try { + IMarker marker = resource.createMarker(markerId); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.SEVERITY, severity); + + // if marker is text type, enforce a line number so that it shows in the editor + // somewhere (line 1) + if (lineNumber < 1 && marker.isSubtypeOf(IMarker.TEXT)) { + lineNumber = 1; + } + + if (lineNumber >= 1) { + marker.setAttribute(IMarker.LINE_NUMBER, lineNumber); + } + + if (startOffset != -1) { + marker.setAttribute(IMarker.CHAR_START, startOffset); + marker.setAttribute(IMarker.CHAR_END, endOffset); + } + + // on Windows, when adding a marker to a project, it takes a refresh for the marker + // to show. In order to fix this we're forcing a refresh of elements receiving + // markers (and only the element, not its children), to force the marker display. + resource.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + + return marker; + } catch (CoreException e) { + AdtPlugin.log(e, "Failed to add marker '%1$s' to '%2$s'", //$NON-NLS-1$ + markerId, resource.getFullPath()); + } + + return null; + } + + /** + * Adds a marker to a resource. This methods catches thrown {@link CoreException}, + * and returns null instead. + * @param resource the file to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param severity the severity of the marker. + * @return the IMarker that was added or null if it failed to add one. + */ + @Nullable + public final static IMarker markResource(IResource resource, String markerId, + String message, int severity) { + return markResource(resource, markerId, message, -1, severity); + } + + /** + * Adds a marker to an {@link IProject}. This method does not catch {@link CoreException}, like + * {@link #markResource(IResource, String, String, int)}. + * + * @param project the project to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param severity the severity of the marker. + * @param priority the priority of the marker + * @return the IMarker that was added. + * @throws CoreException if the marker cannot be added + */ + @Nullable + public final static IMarker markProject(IProject project, String markerId, + String message, int severity, int priority) throws CoreException { + if (!project.isAccessible()) { + return null; + } + + IMarker marker = project.createMarker(markerId); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.SEVERITY, severity); + marker.setAttribute(IMarker.PRIORITY, priority); + + // on Windows, when adding a marker to a project, it takes a refresh for the marker + // to show. In order to fix this we're forcing a refresh of elements receiving + // markers (and only the element, not its children), to force the marker display. + project.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + + return marker; + } + + /** + * Tests that a class name is valid for usage in the manifest. + * <p/> + * This tests the class existence, that it can be instantiated (ie it must not be abstract, + * nor non static if enclosed), and that it extends the proper super class (not necessarily + * directly) + * @param javaProject the {@link IJavaProject} containing the class. + * @param className the fully qualified name of the class to test. + * @param superClassName the fully qualified name of the expected super class. + * @param testVisibility if <code>true</code>, the method will check the visibility of the class + * or of its constructors. + * @return {@link #TEST_CLASS_OK} or an error message. + */ + public final static String testClassForManifest(IJavaProject javaProject, String className, + String superClassName, boolean testVisibility) { + try { + // replace $ by . + String javaClassName = className.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$ + + // look for the IType object for this class + IType type = javaProject.findType(javaClassName); + if (type != null && type.exists()) { + // test that the class is not abstract + int flags = type.getFlags(); + if (Flags.isAbstract(flags)) { + return String.format("%1$s is abstract", className); + } + + // test whether the class is public or not. + if (testVisibility && Flags.isPublic(flags) == false) { + // if its not public, it may have a public default constructor, + // which would then be fine. + IMethod basicConstructor = type.getMethod(type.getElementName(), new String[0]); + if (basicConstructor != null && basicConstructor.exists()) { + int constructFlags = basicConstructor.getFlags(); + if (Flags.isPublic(constructFlags) == false) { + return String.format( + "%1$s or its default constructor must be public for the system to be able to instantiate it", + className); + } + } else { + return String.format( + "%1$s must be public, or the system will not be able to instantiate it.", + className); + } + } + + // If it's enclosed, test that it's static. If its declaring class is enclosed + // as well, test that it is also static, and public. + IType declaringType = type; + do { + IType tmpType = declaringType.getDeclaringType(); + if (tmpType != null) { + if (tmpType.exists()) { + flags = declaringType.getFlags(); + if (Flags.isStatic(flags) == false) { + return String.format("%1$s is enclosed, but not static", + declaringType.getFullyQualifiedName()); + } + + flags = tmpType.getFlags(); + if (testVisibility && Flags.isPublic(flags) == false) { + return String.format("%1$s is not public", + tmpType.getFullyQualifiedName()); + } + } else { + // if it doesn't exist, we need to exit so we may as well mark it null. + tmpType = null; + } + } + declaringType = tmpType; + } while (declaringType != null); + + // test the class inherit from the specified super class. + // get the type hierarchy + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + + // if the super class is not the reference class, it may inherit from + // it so we get its supertype. At some point it will be null and we + // will stop + IType superType = type; + boolean foundProperSuperClass = false; + while ((superType = hierarchy.getSuperclass(superType)) != null && + superType.exists()) { + if (superClassName.equals(superType.getFullyQualifiedName())) { + foundProperSuperClass = true; + } + } + + // didn't find the proper superclass? return false. + if (foundProperSuperClass == false) { + return String.format("%1$s does not extend %2$s", className, superClassName); + } + + return TEST_CLASS_OK; + } else { + return String.format("Class %1$s does not exist", className); + } + } catch (JavaModelException e) { + return String.format("%1$s: %2$s", className, e.getMessage()); + } + } + + /** + * Returns the {@link IJavaProject} for a {@link IProject} object. + * <p/> + * This checks if the project has the Java Nature first. + * @param project + * @return the IJavaProject or null if the project couldn't be created or if the project + * does not have the Java Nature. + * @throws CoreException if this method fails. Reasons include: + * <ul><li>This project does not exist.</li><li>This project is not open.</li></ul> + */ + public static IJavaProject getJavaProject(IProject project) throws CoreException { + if (project != null && project.hasNature(JavaCore.NATURE_ID)) { + return JavaCore.create(project); + } + return null; + } + + /** + * Reveals a specific line in the source file defining a specified class, + * for a specific project. + * @param project + * @param className + * @param line + * @return true if the source was revealed + */ + public static boolean revealSource(IProject project, String className, int line) { + // Inner classes are pointless: All we need is the enclosing type to find the file, and the + // line number. + // Since the anonymous ones will cause IJavaProject#findType to fail, we remove + // all of them. + int pos = className.indexOf('$'); + if (pos != -1) { + className = className.substring(0, pos); + } + + // get the java project + IJavaProject javaProject = JavaCore.create(project); + + try { + // look for the IType matching the class name. + IType result = javaProject.findType(className); + if (result != null && result.exists()) { + // before we show the type in an editor window, we make sure the current + // workbench page has an editor area (typically the ddms perspective doesn't). + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + IWorkbenchPage page = window.getActivePage(); + if (page.isEditorAreaVisible() == false) { + // no editor area? we open the java perspective. + new OpenJavaPerspectiveAction().run(); + } + + IEditorPart editor = JavaUI.openInEditor(result); + if (editor instanceof ITextEditor) { + // get the text editor that was just opened. + ITextEditor textEditor = (ITextEditor)editor; + + IEditorInput input = textEditor.getEditorInput(); + + // get the location of the line to show. + IDocumentProvider documentProvider = textEditor.getDocumentProvider(); + IDocument document = documentProvider.getDocument(input); + IRegion lineInfo = document.getLineInformation(line - 1); + + // select and reveal the line. + textEditor.selectAndReveal(lineInfo.getOffset(), lineInfo.getLength()); + } + + return true; + } + } catch (JavaModelException e) { + } catch (PartInitException e) { + } catch (BadLocationException e) { + } + + return false; + } + + /** + * Returns the list of android-flagged projects. This list contains projects that are opened + * in the workspace and that are flagged as android project (through the android nature) + * @param filter an optional filter to control which android project are returned. Can be null. + * @return an array of IJavaProject, which can be empty if no projects match. + */ + public static @NonNull IJavaProject[] getAndroidProjects(@Nullable IProjectFilter filter) { + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + + return getAndroidProjects(javaModel, filter); + } + + /** + * Returns the list of android-flagged projects for the specified java Model. + * This list contains projects that are opened in the workspace and that are flagged as android + * project (through the android nature) + * @param javaModel the Java Model object corresponding for the current workspace root. + * @param filter an optional filter to control which android project are returned. Can be null. + * @return an array of IJavaProject, which can be empty if no projects match. + */ + @NonNull + public static IJavaProject[] getAndroidProjects(@NonNull IJavaModel javaModel, + @Nullable IProjectFilter filter) { + // get the java projects + IJavaProject[] javaProjectList = null; + try { + javaProjectList = javaModel.getJavaProjects(); + } + catch (JavaModelException jme) { + return new IJavaProject[0]; + } + + // temp list to build the android project array + ArrayList<IJavaProject> androidProjectList = new ArrayList<IJavaProject>(); + + // loop through the projects and add the android flagged projects to the temp list. + for (IJavaProject javaProject : javaProjectList) { + // get the workspace project object + IProject project = javaProject.getProject(); + + // check if it's an android project based on its nature + if (isAndroidProject(project)) { + if (filter == null || filter.accept(project)) { + androidProjectList.add(javaProject); + } + } + } + + // return the android projects list. + return androidProjectList.toArray(new IJavaProject[androidProjectList.size()]); + } + + /** + * Returns true if the given project is an Android project (e.g. is a Java project + * that also has the Android nature) + * + * @param project the project to test + * @return true if the given project is an Android project + */ + public static boolean isAndroidProject(IProject project) { + // check if it's an android project based on its nature + try { + return project.hasNature(AdtConstants.NATURE_DEFAULT); + } catch (CoreException e) { + // this exception, thrown by IProject.hasNature(), means the project either doesn't + // exist or isn't opened. So, in any case we just skip it (the exception will + // bypass the ArrayList.add() + } + + return false; + } + + /** + * Returns the {@link IFolder} representing the output for the project for Android specific + * files. + * <p> + * The project must be a java project and be opened, or the method will return null. + * @param project the {@link IProject} + * @return an IFolder item or null. + */ + public final static IFolder getJavaOutputFolder(IProject project) { + try { + if (project.isOpen() && project.hasNature(JavaCore.NATURE_ID)) { + // get a java project from the normal project object + IJavaProject javaProject = JavaCore.create(project); + + IPath path = javaProject.getOutputLocation(); + path = path.removeFirstSegments(1); + return project.getFolder(path); + } + } catch (JavaModelException e) { + // Let's do nothing and return null + } catch (CoreException e) { + // Let's do nothing and return null + } + return null; + } + + /** + * Returns the {@link IFolder} representing the output for the project for compiled Java + * files. + * <p> + * The project must be a java project and be opened, or the method will return null. + * @param project the {@link IProject} + * @return an IFolder item or null. + */ + @Nullable + public final static IFolder getAndroidOutputFolder(IProject project) { + try { + if (project.isOpen() && project.hasNature(JavaCore.NATURE_ID)) { + return project.getFolder(SdkConstants.FD_OUTPUT); + } + } catch (JavaModelException e) { + // Let's do nothing and return null + } catch (CoreException e) { + // Let's do nothing and return null + } + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java new file mode 100644 index 000000000..56e0c0938 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java @@ -0,0 +1,448 @@ +/* + * 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.project; + +import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidPrintStream; +import com.android.ide.eclipse.adt.internal.build.BuildHelper; +import com.android.ide.eclipse.adt.internal.build.DexException; +import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException; +import com.android.ide.eclipse.adt.internal.build.ProguardExecException; +import com.android.ide.eclipse.adt.internal.build.ProguardResultException; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.build.ApkCreationException; +import com.android.sdklib.build.DuplicateFileException; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.tools.lint.detector.api.LintUtils; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +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 org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * Export helper to export release version of APKs. + */ +public final class ExportHelper { + private static final String HOME_PROPERTY = "user.home"; //$NON-NLS-1$ + private static final String HOME_PROPERTY_REF = "${" + HOME_PROPERTY + '}'; //$NON-NLS-1$ + private static final String SDK_PROPERTY_REF = "${" + PROPERTY_SDK + '}'; //$NON-NLS-1$ + private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$ + + /** + * Exports a release version of the application created by the given project. + * @param project the project to export + * @param outputFile the file to write + * @param key the key to used for signing. Can be null. + * @param certificate the certificate used for signing. Can be null. + * @param monitor progress monitor + * @throws CoreException if an error occurs + */ + public static void exportReleaseApk(IProject project, File outputFile, PrivateKey key, + X509Certificate certificate, IProgressMonitor monitor) throws CoreException { + + // the export, takes the output of the precompiler & Java builders so it's + // important to call build in case the auto-build option of the workspace is disabled. + // Also enable dependency building to make sure everything is up to date. + // However do not package the APK since we're going to do it manually here, using a + // different output location. + ProjectHelper.compileInReleaseMode(project, monitor); + + // if either key or certificate is null, ensure the other is null. + if (key == null) { + certificate = null; + } else if (certificate == null) { + key = null; + } + + try { + // check if the manifest declares debuggable as true. While this is a release build, + // debuggable in the manifest will override this and generate a debug build + IResource manifestResource = project.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML); + if (manifestResource.getType() != IResource.FILE) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("%1$s missing.", SdkConstants.FN_ANDROID_MANIFEST_XML))); + } + + IFileWrapper manifestFile = new IFileWrapper((IFile) manifestResource); + boolean debugMode = AndroidManifest.getDebuggable(manifestFile); + + AndroidPrintStream fakeStream = new AndroidPrintStream(null, null, new OutputStream() { + @Override + public void write(int b) throws IOException { + // do nothing + } + }); + + ProjectState projectState = Sdk.getProjectState(project); + + // get the jumbo mode option + String forceJumboStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_FORCEJUMBO); + Boolean jumbo = Boolean.valueOf(forceJumboStr); + + String dexMergerStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_DISABLE_MERGER); + Boolean dexMerger = Boolean.valueOf(dexMergerStr); + + BuildToolInfo buildToolInfo = getBuildTools(projectState); + + BuildHelper helper = new BuildHelper( + projectState, + buildToolInfo, + fakeStream, fakeStream, + jumbo.booleanValue(), + dexMerger.booleanValue(), + debugMode, false /*verbose*/, + null /*resourceMarker*/); + + // get the list of library projects + List<IProject> libProjects = projectState.getFullLibraryProjects(); + + // Step 1. Package the resources. + + // tmp file for the packaged resource file. To not disturb the incremental builders + // output, all intermediary files are created in tmp files. + File resourceFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_RES); + resourceFile.deleteOnExit(); + + // Make sure the PNG crunch cache is up to date + helper.updateCrunchCache(); + + // get the merged manifest + IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project); + IFile mergedManifestFile = androidOutputFolder.getFile( + SdkConstants.FN_ANDROID_MANIFEST_XML); + + + // package the resources. + helper.packageResources( + mergedManifestFile, + libProjects, + null, // res filter + 0, // versionCode + resourceFile.getParent(), + resourceFile.getName()); + + // Step 2. Convert the byte code to Dalvik bytecode + + // tmp file for the packaged resource file. + File dexFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_DEX); + dexFile.deleteOnExit(); + + ProjectState state = Sdk.getProjectState(project); + String proguardConfig = state.getProperties().getProperty( + ProjectProperties.PROPERTY_PROGUARD_CONFIG); + + boolean runProguard = false; + List<File> proguardConfigFiles = null; + if (proguardConfig != null && proguardConfig.length() > 0) { + // Be tolerant with respect to file and path separators just like + // Ant is. Allow "/" in the property file to mean whatever the file + // separator character is: + if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) { + proguardConfig = proguardConfig.replace('/', File.separatorChar); + } + + Iterable<String> paths = LintUtils.splitPath(proguardConfig); + for (String path : paths) { + if (path.startsWith(SDK_PROPERTY_REF)) { + path = AdtPrefs.getPrefs().getOsSdkFolder() + + path.substring(SDK_PROPERTY_REF.length()); + } else if (path.startsWith(HOME_PROPERTY_REF)) { + path = System.getProperty(HOME_PROPERTY) + + path.substring(HOME_PROPERTY_REF.length()); + } + File proguardConfigFile = new File(path); + if (proguardConfigFile.isAbsolute() == false) { + proguardConfigFile = new File(project.getLocation().toFile(), path); + } + if (proguardConfigFile.isFile()) { + if (proguardConfigFiles == null) { + proguardConfigFiles = new ArrayList<File>(); + } + proguardConfigFiles.add(proguardConfigFile); + runProguard = true; + } else { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Invalid proguard configuration file path " + proguardConfigFile + + " does not exist or is not a regular file", null)); + } + } + + // get the proguard file output by aapt + if (proguardConfigFiles != null) { + IFile proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD); + proguardConfigFiles.add(proguardFile.getLocation().toFile()); + } + } + + Collection<String> dxInput; + + if (runProguard) { + // get all the compiled code paths. This will contain both project output + // folder and jar files. + Collection<String> paths = helper.getCompiledCodePaths(); + + // create a jar file containing all the project output (as proguard cannot + // process folders of .class files). + File inputJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR); + inputJar.deleteOnExit(); + JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar)); + + // a list of the other paths (jar files.) + List<String> jars = new ArrayList<String>(); + + for (String path : paths) { + File root = new File(path); + if (root.isDirectory()) { + addFileToJar(jos, root, root); + } else if (root.isFile()) { + jars.add(path); + } + } + jos.close(); + + // destination file for proguard + File obfuscatedJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR); + obfuscatedJar.deleteOnExit(); + + // run proguard + helper.runProguard(proguardConfigFiles, inputJar, jars, obfuscatedJar, + new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD)); + + helper.setProguardOutput(obfuscatedJar.getAbsolutePath()); + + // dx input is proguard's output + dxInput = Collections.singletonList(obfuscatedJar.getAbsolutePath()); + } else { + // no proguard, simply get all the compiled code path: project output(s) + + // jar file(s) + dxInput = helper.getCompiledCodePaths(); + } + + IJavaProject javaProject = JavaCore.create(project); + + helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath()); + + // Step 3. Final package + + helper.finalPackage( + resourceFile.getAbsolutePath(), + dexFile.getAbsolutePath(), + outputFile.getAbsolutePath(), + libProjects, + key, + certificate, + null); //resourceMarker + + // success! + } catch (CoreException e) { + throw e; + } catch (ProguardResultException e) { + String msg = String.format("Proguard returned with error code %d. See console", + e.getErrorCode()); + AdtPlugin.printErrorToConsole(project, msg); + AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput()); + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + msg, e)); + } catch (ProguardExecException e) { + String msg = String.format("Failed to run proguard: %s", e.getMessage()); + AdtPlugin.printErrorToConsole(project, msg); + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + msg, e)); + } catch (DuplicateFileException e) { + String msg = String.format( + "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", + e.getArchivePath(), e.getFile1(), e.getFile2()); + AdtPlugin.printErrorToConsole(project, msg); + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + e.getMessage(), e)); + } catch (NativeLibInJarException e) { + String msg = e.getMessage(); + + AdtPlugin.printErrorToConsole(project, msg); + AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo()); + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + e.getMessage(), e)); + } catch (DexException e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + e.getMessage(), e)); + } catch (ApkCreationException e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + e.getMessage(), e)); + } catch (Exception e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Failed to export application", e)); + } finally { + // move back to a debug build. + // By using a normal build, we'll simply rebuild the debug version, and let the + // builder decide whether to build the full package or not. + ProjectHelper.buildWithDeps(project, IncrementalProjectBuilder.FULL_BUILD, monitor); + project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + } + + public static BuildToolInfo getBuildTools(ProjectState projectState) + throws CoreException { + BuildToolInfo buildToolInfo = projectState.getBuildToolInfo(); + if (buildToolInfo == null) { + buildToolInfo = Sdk.getCurrent().getLatestBuildTool(); + } + + if (buildToolInfo == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "No Build Tools installed in the SDK.")); + } + return buildToolInfo; + } + + /** + * Exports an unsigned release APK after prompting the user for a location. + * + * <strong>Must be called from the UI thread.</strong> + * + * @param project the project to export + */ + public static void exportUnsignedReleaseApk(final IProject project) { + Shell shell = Display.getCurrent().getActiveShell(); + + // create a default file name for the apk. + String fileName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE; + + // Pop up the file save window to get the file location + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + + fileDialog.setText("Export Project"); + fileDialog.setFileName(fileName); + + final String saveLocation = fileDialog.open(); + if (saveLocation != null) { + new Job("Android Release Export") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + exportReleaseApk(project, + new File(saveLocation), + null, //key + null, //certificate + monitor); + + // this is unsigned export. Let's tell the developers to run zip align + AdtPlugin.displayWarning("Android IDE Plug-in", String.format( + "An unsigned package of the application was saved at\n%1$s\n\n" + + "Before publishing the application you will need to:\n" + + "- Sign the application with your release key,\n" + + "- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" + + "Aligning applications allows Android to use application resources\n" + + "more efficiently.", saveLocation)); + + return Status.OK_STATUS; + } catch (CoreException e) { + AdtPlugin.displayError("Android IDE Plug-in", String.format( + "Error exporting application:\n\n%1$s", e.getMessage())); + return e.getStatus(); + } + } + }.schedule(); + } + } + + /** + * Adds a file to a jar file. + * The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be + * a parent of <var>file</var>. + * @param jar the jar to add the file to + * @param file the file to add + * @param rootDirectory the rootDirectory. + * @throws IOException + */ + private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory) + throws IOException { + if (file.isDirectory()) { + if (file.getName().equals("META-INF") == false) { + for (File child: file.listFiles()) { + addFileToJar(jar, child, rootDirectory); + } + } + } else if (file.isFile()) { + String rootPath = rootDirectory.getAbsolutePath(); + String path = file.getAbsolutePath(); + path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + if (path.charAt(0) == '/') { + path = path.substring(1); + } + + JarEntry entry = new JarEntry(path); + entry.setTime(file.lastModified()); + jar.putNextEntry(entry); + + // put the content of the file. + byte[] buffer = new byte[1024]; + int count; + BufferedInputStream bis = null; + try { + bis = new BufferedInputStream(new FileInputStream(file)); + while ((count = bis.read(buffer)) != -1) { + jar.write(buffer, 0, count); + } + } finally { + if (bis != null) { + try { + bis.close(); + } catch (IOException ignore) { + } + } + } + jar.closeEntry(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java new file mode 100644 index 000000000..e311bfb0b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; + +import java.util.ArrayList; + +/** + * Class to fix the launch configuration of a project if the java package + * defined in the manifest has been changed.<br> + * This fix can be done synchronously, or asynchronously.<br> + * <code>start()</code> will start a thread that will do the fix.<br> + * <code>run()</code> will do the fix in the current thread.<br><br> + * By default, the fix first display a dialog to the user asking if he/she wants to + * do the fix. This can be overriden by calling <code>setDisplayPrompt(false)</code>. + * + */ +public class FixLaunchConfig extends Thread { + + private IProject mProject; + private String mOldPackage; + private String mNewPackage; + + private boolean mDisplayPrompt = true; + + public FixLaunchConfig(IProject project, String oldPackage, String newPackage) { + super(); + + mProject = project; + mOldPackage = oldPackage; + mNewPackage = newPackage; + } + + /** + * Set the display prompt. If true run()/start() first ask the user if he/she wants + * to fix the Launch Config + * @param displayPrompt + */ + public void setDisplayPrompt(boolean displayPrompt) { + mDisplayPrompt = displayPrompt; + } + + /** + * Fix the Launch configurations. + */ + @Override + public void run() { + + if (mDisplayPrompt) { + // ask the user if he really wants to fix the launch config + boolean res = AdtPlugin.displayPrompt( + "Launch Configuration Update", + "The package definition in the manifest changed.\nDo you want to update your Launch Configuration(s)?"); + + if (res == false) { + return; + } + } + + // get the list of config for the project + String projectName = mProject.getName(); + ILaunchConfiguration[] configs = findConfigs(mProject.getName()); + + // loop through all the config and update the package + for (ILaunchConfiguration config : configs) { + try { + // get the working copy so that we can make changes. + ILaunchConfigurationWorkingCopy copy = config.getWorkingCopy(); + + // get the attributes for the activity + String activity = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, + ""); //$NON-NLS-1$ + + // manifests can define activities that are not in the defined package, + // so we need to make sure the activity is inside the old package. + if (activity.startsWith(mOldPackage)) { + // create the new activity + activity = mNewPackage + activity.substring(mOldPackage.length()); + + // put it in the copy + copy.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, activity); + + // save the config + copy.doSave(); + } + } catch (CoreException e) { + // couldn't get the working copy. we output the error in the console + String msg = String.format("Failed to modify %1$s: %2$s", projectName, + e.getMessage()); + AdtPlugin.printErrorToConsole(mProject, msg); + } + } + + } + + /** + * Looks for and returns all existing Launch Configuration object for a + * specified project. + * @param projectName The name of the project + * @return all the ILaunchConfiguration object. If none are present, an empty array is + * returned. + */ + private static ILaunchConfiguration[] findConfigs(String projectName) { + // get the launch manager + ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); + + // now get the config type for our particular android type. + ILaunchConfigurationType configType = manager. + getLaunchConfigurationType(LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); + + // create a temp list to hold all the valid configs + ArrayList<ILaunchConfiguration> list = new ArrayList<ILaunchConfiguration>(); + + try { + ILaunchConfiguration[] configs = manager.getLaunchConfigurations(configType); + + for (ILaunchConfiguration config : configs) { + if (config.getAttribute( + IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, + "").equals(projectName)) { //$NON-NLS-1$ + list.add(config); + } + } + } catch (CoreException e) { + } + + return list.toArray(new ILaunchConfiguration[list.size()]); + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java new file mode 100644 index 000000000..054890f86 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java @@ -0,0 +1,109 @@ +/* + * 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.project; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IDecoration; +import org.eclipse.jface.viewers.ILabelDecorator; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ILightweightLabelDecorator; + +/** + * A {@link ILabelDecorator} associated with an org.eclipse.ui.decorators extension. + * This is used to add android icons in some special folders in the package explorer. + */ +public class FolderDecorator implements ILightweightLabelDecorator { + + private ImageDescriptor mDescriptor; + + public FolderDecorator() { + mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png"); //$NON-NLS-1$ + } + + @Override + public void decorate(Object element, IDecoration decoration) { + if (element instanceof IFolder) { + IFolder folder = (IFolder)element; + + // get the project and make sure this is an android project + IProject project = folder.getProject(); + if (project == null || !project.exists() || !folder.exists()) { + return; + } + + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT)) { + // check the folder is directly under the project. + if (folder.getParent().getType() == IResource.PROJECT) { + String name = folder.getName(); + if (name.equals(SdkConstants.FD_ASSETS)) { + doDecoration(decoration, null); + } else if (name.equals(SdkConstants.FD_RESOURCES)) { + doDecoration(decoration, null); + } else if (name.equals(SdkConstants.FD_GEN_SOURCES)) { + doDecoration(decoration, " [Generated Java Files]"); + } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) { + doDecoration(decoration, null); + } else if (name.equals(SdkConstants.FD_OUTPUT)) { + doDecoration(decoration, null); + } + } + } + } catch (CoreException e) { + // log the error + AdtPlugin.log(e, "Unable to get nature of project '%s'.", project.getName()); + } + } + } + + public void doDecoration(IDecoration decoration, String suffix) { + decoration.addOverlay(mDescriptor, IDecoration.TOP_LEFT); + + if (suffix != null) { + decoration.addSuffix(suffix); + } + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // Property change do not affect the label + return false; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // No state change will affect the rendering. + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // No state change will affect the rendering. + } + + @Override + public void dispose() { + // nothing to dispose + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java new file mode 100644 index 000000000..8fbee4089 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java @@ -0,0 +1,641 @@ +/* + * Copyright (C) 2011 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.project; + +import static com.android.ide.eclipse.adt.AdtConstants.CONTAINER_DEPENDENCIES; + +import com.android.SdkConstants; +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.AndroidPrintStream; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.build.JarListSanitizer; +import com.android.sdklib.build.JarListSanitizer.DifferentLibException; +import com.android.sdklib.build.JarListSanitizer.Sha1Exception; +import com.android.sdklib.build.RenderScriptProcessor; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +public class LibraryClasspathContainerInitializer extends BaseClasspathContainerInitializer { + + private final static String ATTR_SRC = "src"; //$NON-NLS-1$ + private final static String ATTR_DOC = "doc"; //$NON-NLS-1$ + private final static String DOT_PROPERTIES = ".properties"; //$NON-NLS-1$ + + public LibraryClasspathContainerInitializer() { + } + + /** + * Updates the {@link IJavaProject} objects with new library. + * @param androidProjects the projects to update. + * @return <code>true</code> if success, <code>false</code> otherwise. + */ + public static boolean updateProjects(IJavaProject[] androidProjects) { + try { + // Allocate a new AndroidClasspathContainer, and associate it to the library + // container id for each projects. + int projectCount = androidProjects.length; + + IClasspathContainer[] libraryContainers = new IClasspathContainer[projectCount]; + IClasspathContainer[] dependencyContainers = new IClasspathContainer[projectCount]; + for (int i = 0 ; i < projectCount; i++) { + libraryContainers[i] = allocateLibraryContainer(androidProjects[i]); + dependencyContainers[i] = allocateDependencyContainer(androidProjects[i]); + } + + // give each project their new container in one call. + JavaCore.setClasspathContainer( + new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), + androidProjects, libraryContainers, new NullProgressMonitor()); + + JavaCore.setClasspathContainer( + new Path(AdtConstants.CONTAINER_DEPENDENCIES), + androidProjects, dependencyContainers, new NullProgressMonitor()); + return true; + } catch (JavaModelException e) { + return false; + } + } + + /** + * Updates the {@link IJavaProject} objects with new library. + * @param androidProjects the projects to update. + * @return <code>true</code> if success, <code>false</code> otherwise. + */ + public static boolean updateProject(List<ProjectState> projects) { + List<IJavaProject> javaProjectList = new ArrayList<IJavaProject>(projects.size()); + for (ProjectState p : projects) { + IJavaProject javaProject = JavaCore.create(p.getProject()); + if (javaProject != null) { + javaProjectList.add(javaProject); + } + } + + IJavaProject[] javaProjects = javaProjectList.toArray( + new IJavaProject[javaProjectList.size()]); + + return updateProjects(javaProjects); + } + + @Override + public void initialize(IPath containerPath, IJavaProject project) throws CoreException { + if (AdtConstants.CONTAINER_PRIVATE_LIBRARIES.equals(containerPath.toString())) { + IClasspathContainer libraries = allocateLibraryContainer(project); + if (libraries != null) { + JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), + new IJavaProject[] { project }, + new IClasspathContainer[] { libraries }, + new NullProgressMonitor()); + } + + } else if(AdtConstants.CONTAINER_DEPENDENCIES.equals(containerPath.toString())) { + IClasspathContainer dependencies = allocateDependencyContainer(project); + if (dependencies != null) { + JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_DEPENDENCIES), + new IJavaProject[] { project }, + new IClasspathContainer[] { dependencies }, + new NullProgressMonitor()); + } + } + } + + private static IClasspathContainer allocateLibraryContainer(IJavaProject javaProject) { + final IProject iProject = javaProject.getProject(); + + // check if the project has a valid target. + ProjectState state = Sdk.getProjectState(iProject); + if (state == null) { + // getProjectState should already have logged an error. Just bail out. + return null; + } + + /* + * At this point we're going to gather a list of all that need to go in the + * dependency container. + * - Library project outputs (direct and indirect) + * - Java project output (those can be indirectly referenced through library projects + * or other other Java projects) + * - Jar files: + * + inside this project's libs/ + * + inside the library projects' libs/ + * + inside the referenced Java projects' classpath + */ + List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); + + // list of java project dependencies and jar files that will be built while + // going through the library projects. + Set<File> jarFiles = new HashSet<File>(); + Set<IProject> refProjects = new HashSet<IProject>(); + + // process all the libraries + + List<IProject> libProjects = state.getFullLibraryProjects(); + for (IProject libProject : libProjects) { + // process all of the library project's dependencies + getDependencyListFromClasspath(libProject, refProjects, jarFiles, true); + } + + // now process this projects' referenced projects only. + processReferencedProjects(iProject, refProjects, jarFiles); + + // and the content of its libs folder + getJarListFromLibsFolder(iProject, jarFiles); + + // now add a classpath entry for each Java project (this is a set so dups are already + // removed) + for (IProject p : refProjects) { + entries.add(JavaCore.newProjectEntry(p.getFullPath(), true /*isExported*/)); + } + + entries.addAll(convertJarsToClasspathEntries(iProject, jarFiles)); + + return allocateContainer(javaProject, entries, new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), + "Android Private Libraries"); + } + + private static List<IClasspathEntry> convertJarsToClasspathEntries(final IProject iProject, + Set<File> jarFiles) { + List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(jarFiles.size()); + + // and process the jar files list, but first sanitize it to remove dups. + JarListSanitizer sanitizer = new JarListSanitizer( + iProject.getFolder(SdkConstants.FD_OUTPUT).getLocation().toFile(), + new AndroidPrintStream(iProject, null /*prefix*/, + AdtPlugin.getOutStream())); + + String errorMessage = null; + + try { + List<File> sanitizedList = sanitizer.sanitize(jarFiles); + + for (File jarFile : sanitizedList) { + if (jarFile instanceof CPEFile) { + CPEFile cpeFile = (CPEFile) jarFile; + IClasspathEntry e = cpeFile.getClasspathEntry(); + + entries.add(JavaCore.newLibraryEntry( + e.getPath(), + e.getSourceAttachmentPath(), + e.getSourceAttachmentRootPath(), + e.getAccessRules(), + e.getExtraAttributes(), + true /*isExported*/)); + } else { + String jarPath = jarFile.getAbsolutePath(); + + IPath sourceAttachmentPath = null; + IClasspathAttribute javaDocAttribute = null; + + File jarProperties = new File(jarPath + DOT_PROPERTIES); + if (jarProperties.isFile()) { + Properties p = new Properties(); + InputStream is = null; + try { + p.load(is = new FileInputStream(jarProperties)); + + String value = p.getProperty(ATTR_SRC); + if (value != null) { + File srcPath = getFile(jarFile, value); + + if (srcPath.exists()) { + sourceAttachmentPath = new Path(srcPath.getAbsolutePath()); + } + } + + value = p.getProperty(ATTR_DOC); + if (value != null) { + File docPath = getFile(jarFile, value); + if (docPath.exists()) { + try { + javaDocAttribute = JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + docPath.toURI().toURL().toString()); + } catch (MalformedURLException e) { + AdtPlugin.log(e, "Failed to process 'doc' attribute for %s", + jarProperties.getAbsolutePath()); + } + } + } + + } catch (FileNotFoundException e) { + // shouldn't happen since we check upfront + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read %s", jarProperties.getAbsolutePath()); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + if (javaDocAttribute != null) { + entries.add(JavaCore.newLibraryEntry(new Path(jarPath), + sourceAttachmentPath, null /*sourceAttachmentRootPath*/, + new IAccessRule[0], + new IClasspathAttribute[] { javaDocAttribute }, + true /*isExported*/)); + } else { + entries.add(JavaCore.newLibraryEntry(new Path(jarPath), + sourceAttachmentPath, null /*sourceAttachmentRootPath*/, + true /*isExported*/)); + } + } + } + } catch (DifferentLibException e) { + errorMessage = e.getMessage(); + AdtPlugin.printErrorToConsole(iProject, (Object[]) e.getDetails()); + } catch (Sha1Exception e) { + errorMessage = e.getMessage(); + } + + processError(iProject, errorMessage, AdtConstants.MARKER_DEPENDENCY, + true /*outputToConsole*/); + + return entries; + } + + private static IClasspathContainer allocateDependencyContainer(IJavaProject javaProject) { + final IProject iProject = javaProject.getProject(); + final List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); + final Set<File> jarFiles = new HashSet<File>(); + final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + + AdtPlugin plugin = AdtPlugin.getDefault(); + if (plugin == null) { // This is totally weird, but I've seen it happen! + return null; + } + + synchronized (Sdk.getLock()) { + boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; + + // check if the project has a valid target. + final ProjectState state = Sdk.getProjectState(iProject); + if (state == null) { + // getProjectState should already have logged an error. Just bail out. + return null; + } + + // annotations support for older version of android + if (state.getTarget() != null && state.getTarget().getVersion().getApiLevel() <= 15) { + File annotationsJar = new File(Sdk.getCurrent().getSdkOsLocation(), + SdkConstants.FD_TOOLS + File.separator + SdkConstants.FD_SUPPORT + + File.separator + SdkConstants.FN_ANNOTATIONS_JAR); + + jarFiles.add(annotationsJar); + } + + if (state.getRenderScriptSupportMode()) { + if (!sdkIsLoaded) { + return null; + } + BuildToolInfo buildToolInfo = state.getBuildToolInfo(); + if (buildToolInfo == null) { + buildToolInfo = Sdk.getCurrent().getLatestBuildTool(); + + if (buildToolInfo == null) { + return null; + } + } + + File renderScriptSupportJar = RenderScriptProcessor.getSupportJar( + buildToolInfo.getLocation().getAbsolutePath()); + + jarFiles.add(renderScriptSupportJar); + } + + // process all the libraries + + List<IProject> libProjects = state.getFullLibraryProjects(); + for (IProject libProject : libProjects) { + // get the project output + IFolder outputFolder = BaseProjectHelper.getAndroidOutputFolder(libProject); + + if (outputFolder != null) { // can happen when closing/deleting a library) + IFile jarIFile = outputFolder.getFile(libProject.getName().toLowerCase() + + SdkConstants.DOT_JAR); + + // get the source folder for the library project + List<IPath> srcs = BaseProjectHelper.getSourceClasspaths(libProject); + // find the first non-derived source folder. + IPath sourceFolder = null; + for (IPath src : srcs) { + IFolder srcFolder = workspaceRoot.getFolder(src); + if (srcFolder.isDerived() == false) { + sourceFolder = src; + break; + } + } + + // we can directly add a CPE for this jar as there's no risk of a duplicate. + IClasspathEntry entry = JavaCore.newLibraryEntry( + jarIFile.getLocation(), + sourceFolder, // source attachment path + null, // default source attachment root path. + true /*isExported*/); + + entries.add(entry); + } + } + + entries.addAll(convertJarsToClasspathEntries(iProject, jarFiles)); + + return allocateContainer(javaProject, entries, new Path(CONTAINER_DEPENDENCIES), + "Android Dependencies"); + } + } + + private static IClasspathContainer allocateContainer(IJavaProject javaProject, + List<IClasspathEntry> entries, IPath id, String description) { + + if (AdtPlugin.getDefault() == null) { // This is totally weird, but I've seen it happen! + return null; + } + + // First check that the project has a library-type container. + try { + IClasspathEntry[] rawClasspath = javaProject.getRawClasspath(); + final IClasspathEntry[] oldRawClasspath = rawClasspath; + + boolean foundContainer = false; + for (IClasspathEntry entry : rawClasspath) { + // get the entry and kind + final int kind = entry.getEntryKind(); + + if (kind == IClasspathEntry.CPE_CONTAINER) { + String path = entry.getPath().toString(); + String idString = id.toString(); + if (idString.equals(path)) { + foundContainer = true; + break; + } + } + } + + // if there isn't any, add it. + if (foundContainer == false) { + // add the android container to the array + rawClasspath = ProjectHelper.addEntryToClasspath(rawClasspath, + JavaCore.newContainerEntry(id, true /*isExported*/)); + } + + // set the new list of entries to the project + if (rawClasspath != oldRawClasspath) { + javaProject.setRawClasspath(rawClasspath, new NullProgressMonitor()); + } + } catch (JavaModelException e) { + // This really shouldn't happen, but if it does, simply return null (the calling + // method will fails as well) + return null; + } + + return new AndroidClasspathContainer( + entries.toArray(new IClasspathEntry[entries.size()]), + id, + description, + IClasspathContainer.K_APPLICATION); + } + + private static File getFile(File root, String value) { + File file = new File(value); + if (file.isAbsolute() == false) { + file = new File(root.getParentFile(), value); + } + + return file; + } + + /** + * Finds all the jar files inside a project's libs folder. + * @param project + * @param jarFiles + */ + private static void getJarListFromLibsFolder(IProject project, Set<File> jarFiles) { + IFolder libsFolder = project.getFolder(SdkConstants.FD_NATIVE_LIBS); + if (libsFolder.exists()) { + try { + IResource[] members = libsFolder.members(); + for (IResource member : members) { + if (member.getType() == IResource.FILE && + SdkConstants.EXT_JAR.equalsIgnoreCase(member.getFileExtension())) { + IPath location = member.getLocation(); + if (location != null) { + jarFiles.add(location.toFile()); + } + } + } + } catch (CoreException e) { + // can't get the list? ignore this folder. + } + } + } + + /** + * Process reference projects from the main projects to add indirect dependencies coming + * from Java project. + * @param project the main project + * @param projects the project list to add to + * @param jarFiles the jar list to add to. + */ + private static void processReferencedProjects(IProject project, + Set<IProject> projects, Set<File> jarFiles) { + try { + IProject[] refs = project.getReferencedProjects(); + for (IProject p : refs) { + // ignore if it's an Android project, or if it's not a Java + // Project + if (p.hasNature(JavaCore.NATURE_ID) + && p.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + + // process this project's dependencies + getDependencyListFromClasspath(p, projects, jarFiles, true /*includeJarFiles*/); + } + } + } catch (CoreException e) { + // can't get the referenced projects? ignore + } + } + + /** + * Finds all the dependencies of a given project and add them to a project list and + * a jar list. + * Only classpath entries that are exported are added, and only Java project (not Android + * project) are added. + * + * @param project the project to query + * @param projects the referenced project list to add to + * @param jarFiles the jar list to add to + * @param includeJarFiles whether to include jar files or just projects. This is useful when + * calling on an Android project (value should be <code>false</code>) + */ + private static void getDependencyListFromClasspath(IProject project, Set<IProject> projects, + Set<File> jarFiles, boolean includeJarFiles) { + IJavaProject javaProject = JavaCore.create(project); + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + + // we could use IJavaProject.getResolvedClasspath directly, but we actually + // want to see the containers themselves. + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + // ignore entries that are not exported + if (!e.getPath().toString().equals(CONTAINER_DEPENDENCIES) && e.isExported()) { + processCPE(e, javaProject, wsRoot, projects, jarFiles, includeJarFiles); + } + } + } + } + + /** + * Processes a {@link IClasspathEntry} and add it to one of the list if applicable. + * @param entry the entry to process + * @param javaProject the {@link IJavaProject} from which this entry came. + * @param wsRoot the {@link IWorkspaceRoot} + * @param projects the project list to add to + * @param jarFiles the jar list to add to + * @param includeJarFiles whether to include jar files or just projects. This is useful when + * calling on an Android project (value should be <code>false</code>) + */ + private static void processCPE(IClasspathEntry entry, IJavaProject javaProject, + IWorkspaceRoot wsRoot, + Set<IProject> projects, Set<File> jarFiles, boolean includeJarFiles) { + + // if this is a classpath variable reference, we resolve it. + if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + entry = JavaCore.getResolvedClasspathEntry(entry); + } + + if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { + IProject refProject = wsRoot.getProject(entry.getPath().lastSegment()); + try { + // ignore if it's an Android project, or if it's not a Java Project + if (refProject.hasNature(JavaCore.NATURE_ID) && + refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + // add this project to the list + projects.add(refProject); + + // also get the dependency from this project. + getDependencyListFromClasspath(refProject, projects, jarFiles, + true /*includeJarFiles*/); + } + } catch (CoreException exception) { + // can't query the project nature? ignore + } + } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { + if (includeJarFiles) { + handleClasspathLibrary(entry, wsRoot, jarFiles); + } + } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + // get the container and its content + try { + IClasspathContainer container = JavaCore.getClasspathContainer( + entry.getPath(), javaProject); + // ignore the system and default_system types as they represent + // libraries that are part of the runtime. + if (container != null && + container.getKind() == IClasspathContainer.K_APPLICATION) { + IClasspathEntry[] entries = container.getClasspathEntries(); + for (IClasspathEntry cpe : entries) { + processCPE(cpe, javaProject, wsRoot, projects, jarFiles, includeJarFiles); + } + } + } catch (JavaModelException jme) { + // can't resolve the container? ignore it. + AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath()); + } + } + } + + private static final class CPEFile extends File { + private static final long serialVersionUID = 1L; + + private final IClasspathEntry mClasspathEntry; + + public CPEFile(String pathname, IClasspathEntry classpathEntry) { + super(pathname); + mClasspathEntry = classpathEntry; + } + + public CPEFile(File file, IClasspathEntry classpathEntry) { + super(file.getAbsolutePath()); + mClasspathEntry = classpathEntry; + } + + public IClasspathEntry getClasspathEntry() { + return mClasspathEntry; + } + } + + private static void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, + Set<File> jarFiles) { + // get the IPath + IPath path = e.getPath(); + + IResource resource = wsRoot.findMember(path); + + if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + // case of a jar file (which could be relative to the workspace or a full path) + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + jarFiles.add(new CPEFile(resource.getLocation().toFile(), e)); + } else { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new CPEFile(osFullPath, e); + if (f.isFile()) { + jarFiles.add(f); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectChooserHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectChooserHelper.java new file mode 100644 index 000000000..9de8ad06e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectChooserHelper.java @@ -0,0 +1,304 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ui.JavaElementLabelProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.ElementListSelectionDialog; + +/** + * Helper class to deal with displaying a project choosing dialog that lists only the + * projects with the Android nature. + */ +public class ProjectChooserHelper { + + private final Shell mParentShell; + private final IProjectChooserFilter mFilter; + + /** + * List of current android projects. Since the dialog is modal, we'll just get + * the list once on-demand. + */ + private IJavaProject[] mAndroidProjects; + + /** + * Interface to filter out some project displayed by {@link ProjectChooserHelper}. + * + * @see IProjectFilter + */ + public interface IProjectChooserFilter extends IProjectFilter { + /** + * Whether the Project Chooser can compute the project list once and cache the result. + * </p>If false the project list is recomputed every time the dialog is opened. + */ + boolean useCache(); + } + + /** + * An implementation of {@link IProjectChooserFilter} that only displays non-library projects. + */ + public final static class NonLibraryProjectOnlyFilter implements IProjectChooserFilter { + @Override + public boolean accept(IProject project) { + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + return state.isLibrary() == false; + } + + return false; + } + + @Override + public boolean useCache() { + return true; + } + } + + /** + * An implementation of {@link IProjectChooserFilter} that only displays library projects. + */ + public final static class LibraryProjectOnlyFilter implements IProjectChooserFilter { + @Override + public boolean accept(IProject project) { + ProjectState state = Sdk.getProjectState(project); + if (state != null ) { + return state.isLibrary(); + } + + return false; + } + + @Override + public boolean useCache() { + return true; + } + } + + /** + * Creates a new project chooser. + * @param parentShell the parent {@link Shell} for the dialog. + * @param filter a filter to only accept certain projects. Can be null. + */ + public ProjectChooserHelper(Shell parentShell, IProjectChooserFilter filter) { + mParentShell = parentShell; + mFilter = filter; + } + + /** + * Displays a project chooser dialog which lists all available projects with the Android nature. + * <p/> + * The list of project is built from Android flagged projects currently opened in the workspace. + * + * @param projectName If non null and not empty, represents the name of an Android project + * that will be selected by default. + * @param message Message for the dialog box. Can be null in which case a default message + * is displayed. + * @return the project chosen by the user in the dialog, or null if the dialog was canceled. + */ + public IJavaProject chooseJavaProject(String projectName, String message) { + ILabelProvider labelProvider = new JavaElementLabelProvider( + JavaElementLabelProvider.SHOW_DEFAULT); + ElementListSelectionDialog dialog = new ElementListSelectionDialog( + mParentShell, labelProvider); + dialog.setTitle("Project Selection"); + if (message == null) { + message = "Please select a project"; + } + dialog.setMessage(message); + + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + + // set the elements in the dialog. These are opened android projects. + dialog.setElements(getAndroidProjects(javaModel)); + + // look for the project matching the given project name + IJavaProject javaProject = null; + if (projectName != null && projectName.length() > 0) { + javaProject = javaModel.getJavaProject(projectName); + } + + // if we found it, we set the initial selection in the dialog to this one. + if (javaProject != null) { + dialog.setInitialSelections(new Object[] { javaProject }); + } + + // open the dialog and return the object selected if OK was clicked, or null otherwise + if (dialog.open() == Window.OK) { + return (IJavaProject) dialog.getFirstResult(); + } + return null; + } + + /** + * Returns the list of Android projects. + * <p/> + * Because this list can be time consuming, this class caches the list of project. + * It is recommended to call this method instead of + * {@link BaseProjectHelper#getAndroidProjects()}. + * + * @param javaModel the java model. Can be null. + */ + public IJavaProject[] getAndroidProjects(IJavaModel javaModel) { + // recompute only if we don't have the projects already or the filter is dynamic + // and prevent usage of a cache. + if (mAndroidProjects == null || (mFilter != null && mFilter.useCache() == false)) { + if (javaModel == null) { + mAndroidProjects = BaseProjectHelper.getAndroidProjects(mFilter); + } else { + mAndroidProjects = BaseProjectHelper.getAndroidProjects(javaModel, mFilter); + } + } + + return mAndroidProjects; + } + + /** + * Helper method to get the Android project with the given name + * + * @param projectName the name of the project to find + * @return the {@link IProject} for the Android project. <code>null</code> if not found. + */ + public IProject getAndroidProject(String projectName) { + IProject iproject = null; + IJavaProject[] javaProjects = getAndroidProjects(null); + if (javaProjects != null) { + for (IJavaProject javaProject : javaProjects) { + if (javaProject.getElementName().equals(projectName)) { + iproject = javaProject.getProject(); + break; + } + } + } + return iproject; + } + + /** + * A selector combo for showing the currently selected project and for + * changing the selection + */ + public static class ProjectCombo extends Combo implements SelectionListener { + /** Currently chosen project, or null when no project has been initialized or selected */ + private IProject mProject; + private IJavaProject[] mAvailableProjects; + + /** + * Creates a new project selector combo + * + * @param helper associated {@link ProjectChooserHelper} for looking up + * projects + * @param parent parent composite to add the combo to + * @param initialProject the initial project to select, or null (which + * will show a "Please Choose Project..." label instead.) + */ + public ProjectCombo(ProjectChooserHelper helper, Composite parent, + IProject initialProject) { + super(parent, SWT.BORDER | SWT.FLAT | SWT.READ_ONLY); + mProject = initialProject; + + mAvailableProjects = helper.getAndroidProjects(null); + String[] items = new String[mAvailableProjects.length + 1]; + items[0] = "--- Choose Project ---"; + + ILabelProvider labelProvider = new JavaElementLabelProvider( + JavaElementLabelProvider.SHOW_DEFAULT); + int selectionIndex = 0; + for (int i = 0, n = mAvailableProjects.length; i < n; i++) { + IProject project = mAvailableProjects[i].getProject(); + items[i + 1] = labelProvider.getText(project); + if (project == initialProject) { + selectionIndex = i + 1; + } + } + setItems(items); + select(selectionIndex); + + addSelectionListener(this); + } + + /** + * Returns the project selected by this chooser (or the initial project + * passed to the constructor if the user did not change it) + * + * @return the selected project + */ + public IProject getSelectedProject() { + return mProject; + } + + /** + * Sets the project selected by this chooser + * + * @param project the selected project + */ + public void setSelectedProject(IProject project) { + mProject = project; + + int selectionIndex = 0; + for (int i = 0, n = mAvailableProjects.length; i < n; i++) { + if (project == mAvailableProjects[i].getProject()) { + selectionIndex = i + 1; // +1: Slot 0 is reserved for "Choose Project" + select(selectionIndex); + break; + } + } + } + + /** + * Click handler for the button: Open the {@link ProjectChooserHelper} + * dialog for selecting a new project. + */ + @Override + public void widgetSelected(SelectionEvent e) { + int selectionIndex = getSelectionIndex(); + if (selectionIndex > 0 && mAvailableProjects != null + && selectionIndex <= mAvailableProjects.length) { + // selection index 0 is "Choose Project", all other projects are offset + // by 1 from the selection index + mProject = mAvailableProjects[selectionIndex - 1].getProject(); + } else { + mProject = null; + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java new file mode 100644 index 000000000..a32b4ca8b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java @@ -0,0 +1,1153 @@ +/* + * Copyright (C) 2007 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.project; + +import static com.android.ide.eclipse.adt.AdtConstants.COMPILER_COMPLIANCE_PREFERRED; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.builders.PostCompilerBuilder; +import com.android.ide.eclipse.adt.internal.build.builders.PreCompilerBuilder; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +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.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.corext.util.JavaModelUtil; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.IVMInstall2; +import org.eclipse.jdt.launching.IVMInstallType; +import org.eclipse.jdt.launching.JavaRuntime; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Utility class to manipulate Project parameters/properties. + */ +public final class ProjectHelper { + public final static int COMPILER_COMPLIANCE_OK = 0; + public final static int COMPILER_COMPLIANCE_LEVEL = 1; + public final static int COMPILER_COMPLIANCE_SOURCE = 2; + public final static int COMPILER_COMPLIANCE_CODEGEN_TARGET = 3; + + /** + * Adds the given ClasspathEntry object to the class path entries. + * This method does not check whether the entry is already defined in the project. + * + * @param entries The class path entries to read. A copy will be returned. + * @param newEntry The new class path entry to add. + * @return A new class path entries array. + */ + public static IClasspathEntry[] addEntryToClasspath( + IClasspathEntry[] entries, IClasspathEntry newEntry) { + int n = entries.length; + IClasspathEntry[] newEntries = new IClasspathEntry[n + 1]; + System.arraycopy(entries, 0, newEntries, 0, n); + newEntries[n] = newEntry; + return newEntries; + } + + /** + * Replaces the given ClasspathEntry in the classpath entries. + * + * If the classpath does not yet exists (Check is based on entry path), then it is added. + * + * @param entries The class path entries to read. The same array (replace) or a copy (add) + * will be returned. + * @param newEntry The new class path entry to add. + * @return The same array (replace) or a copy (add) will be returned. + * + * @see IClasspathEntry#getPath() + */ + public static IClasspathEntry[] replaceEntryInClasspath( + IClasspathEntry[] entries, IClasspathEntry newEntry) { + + IPath path = newEntry.getPath(); + for (int i = 0, count = entries.length; i < count ; i++) { + if (path.equals(entries[i].getPath())) { + entries[i] = newEntry; + return entries; + } + } + + return addEntryToClasspath(entries, newEntry); + } + + /** + * Adds the corresponding source folder to the project's class path entries. + * This method does not check whether the entry is already defined in the project. + * + * @param javaProject The java project of which path entries to update. + * @param newEntry The new class path entry to add. + * @throws JavaModelException + */ + public static void addEntryToClasspath(IJavaProject javaProject, IClasspathEntry newEntry) + throws JavaModelException { + + IClasspathEntry[] entries = javaProject.getRawClasspath(); + entries = addEntryToClasspath(entries, newEntry); + javaProject.setRawClasspath(entries, new NullProgressMonitor()); + } + + /** + * Checks whether the given class path entry is already defined in the project. + * + * @param javaProject The java project of which path entries to check. + * @param newEntry The parent source folder to remove. + * @return True if the class path entry is already defined. + * @throws JavaModelException + */ + public static boolean isEntryInClasspath(IJavaProject javaProject, IClasspathEntry newEntry) + throws JavaModelException { + + IClasspathEntry[] entries = javaProject.getRawClasspath(); + for (IClasspathEntry entry : entries) { + if (entry.equals(newEntry)) { + return true; + } + } + return false; + } + + /** + * Remove a classpath entry from the array. + * @param entries The class path entries to read. A copy will be returned + * @param index The index to remove. + * @return A new class path entries array. + */ + public static IClasspathEntry[] removeEntryFromClasspath( + IClasspathEntry[] entries, int index) { + int n = entries.length; + IClasspathEntry[] newEntries = new IClasspathEntry[n-1]; + + // copy the entries before index + System.arraycopy(entries, 0, newEntries, 0, index); + + // copy the entries after index + System.arraycopy(entries, index + 1, newEntries, index, + entries.length - index - 1); + + return newEntries; + } + + /** + * Converts a OS specific path into a path valid for the java doc location + * attributes of a project. + * @param javaDocOSLocation The OS specific path. + * @return a valid path for the java doc location. + */ + public static String getJavaDocPath(String javaDocOSLocation) { + // first thing we do is convert the \ into / + String javaDoc = javaDocOSLocation.replaceAll("\\\\", //$NON-NLS-1$ + AdtConstants.WS_SEP); + + // then we add file: at the beginning for unix path, and file:/ for non + // unix path + if (javaDoc.startsWith(AdtConstants.WS_SEP)) { + return "file:" + javaDoc; //$NON-NLS-1$ + } + + return "file:/" + javaDoc; //$NON-NLS-1$ + } + + /** + * Look for a specific classpath entry by full path and return its index. + * @param entries The entry array to search in. + * @param entryPath The OS specific path of the entry. + * @param entryKind The kind of the entry. Accepted values are 0 + * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, + * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, + * and IClasspathEntry.CPE_CONTAINER + * @return the index of the found classpath entry or -1. + */ + public static int findClasspathEntryByPath(IClasspathEntry[] entries, + String entryPath, int entryKind) { + for (int i = 0 ; i < entries.length ; i++) { + IClasspathEntry entry = entries[i]; + + int kind = entry.getEntryKind(); + + if (kind == entryKind || entryKind == 0) { + // get the path + IPath path = entry.getPath(); + + String osPathString = path.toOSString(); + if (osPathString.equals(entryPath)) { + return i; + } + } + } + + // not found, return bad index. + return -1; + } + + /** + * Look for a specific classpath entry for file name only and return its + * index. + * @param entries The entry array to search in. + * @param entryName The filename of the entry. + * @param entryKind The kind of the entry. Accepted values are 0 + * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, + * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, + * and IClasspathEntry.CPE_CONTAINER + * @param startIndex Index where to start the search + * @return the index of the found classpath entry or -1. + */ + public static int findClasspathEntryByName(IClasspathEntry[] entries, + String entryName, int entryKind, int startIndex) { + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex ; i < entries.length ; i++) { + IClasspathEntry entry = entries[i]; + + int kind = entry.getEntryKind(); + + if (kind == entryKind || entryKind == 0) { + // get the path + IPath path = entry.getPath(); + String name = path.segment(path.segmentCount()-1); + + if (name.equals(entryName)) { + return i; + } + } + } + + // not found, return bad index. + return -1; + } + + public static boolean updateProject(IJavaProject project) { + return updateProjects(new IJavaProject[] { project}); + } + + /** + * Update the android-specific projects's classpath containers. + * @param projects the projects to update + * @return + */ + public static boolean updateProjects(IJavaProject[] projects) { + boolean r = AndroidClasspathContainerInitializer.updateProjects(projects); + if (r) { + return LibraryClasspathContainerInitializer.updateProjects(projects); + } + return false; + } + + /** + * Fix the project. This checks the SDK location. + * @param project The project to fix. + * @throws JavaModelException + */ + public static void fixProject(IProject project) throws JavaModelException { + if (AdtPlugin.getOsSdkFolder().length() == 0) { + AdtPlugin.printToConsole(project, "Unknown SDK Location, project not fixed."); + return; + } + + // get a java project + IJavaProject javaProject = JavaCore.create(project); + fixProjectClasspathEntries(javaProject); + } + + /** + * Fix the project classpath entries. The method ensures that: + * <ul> + * <li>The project does not reference any old android.zip/android.jar archive.</li> + * <li>The project does not use its output folder as a sourc folder.</li> + * <li>The project does not reference a desktop JRE</li> + * <li>The project references the AndroidClasspathContainer. + * </ul> + * @param javaProject The project to fix. + * @throws JavaModelException + */ + public static void fixProjectClasspathEntries(IJavaProject javaProject) + throws JavaModelException { + + // get the project classpath + IClasspathEntry[] entries = javaProject.getRawClasspath(); + IClasspathEntry[] oldEntries = entries; + boolean forceRewriteOfCPE = false; + + // check if the JRE is set as library + int jreIndex = ProjectHelper.findClasspathEntryByPath(entries, JavaRuntime.JRE_CONTAINER, + IClasspathEntry.CPE_CONTAINER); + if (jreIndex != -1) { + // the project has a JRE included, we remove it + entries = ProjectHelper.removeEntryFromClasspath(entries, jreIndex); + } + + // get the output folder + IPath outputFolder = javaProject.getOutputLocation(); + + boolean foundFrameworkContainer = false; + IClasspathEntry foundLibrariesContainer = null; + IClasspathEntry foundDependenciesContainer = null; + + for (int i = 0 ; i < entries.length ;) { + // get the entry and kind + IClasspathEntry entry = entries[i]; + int kind = entry.getEntryKind(); + + if (kind == IClasspathEntry.CPE_SOURCE) { + IPath path = entry.getPath(); + + if (path.equals(outputFolder)) { + entries = ProjectHelper.removeEntryFromClasspath(entries, i); + + // continue, to skip the i++; + continue; + } + } else if (kind == IClasspathEntry.CPE_CONTAINER) { + String path = entry.getPath().toString(); + if (AdtConstants.CONTAINER_FRAMEWORK.equals(path)) { + foundFrameworkContainer = true; + } else if (AdtConstants.CONTAINER_PRIVATE_LIBRARIES.equals(path)) { + foundLibrariesContainer = entry; + } else if (AdtConstants.CONTAINER_DEPENDENCIES.equals(path)) { + foundDependenciesContainer = entry; + } + } + + i++; + } + + // look to see if we have the m2eclipse nature + boolean m2eNature = false; + try { + m2eNature = javaProject.getProject().hasNature("org.eclipse.m2e.core.maven2Nature"); + } catch (CoreException e) { + AdtPlugin.log(e, "Failed to query project %s for m2e nature", + javaProject.getProject().getName()); + } + + + // if the framework container is not there, we add it + if (!foundFrameworkContainer) { + // add the android container to the array + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newContainerEntry(new Path(AdtConstants.CONTAINER_FRAMEWORK))); + } + + // same thing for the library container + if (foundLibrariesContainer == null) { + // add the exported libraries android container to the array + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newContainerEntry( + new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), true)); + } else if (!m2eNature && !foundLibrariesContainer.isExported()) { + // the container is present but it's not exported and since there's no m2e nature + // we do want it to be exported. + // keep all the other parameters the same. + entries = ProjectHelper.replaceEntryInClasspath(entries, + JavaCore.newContainerEntry( + new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), + foundLibrariesContainer.getAccessRules(), + foundLibrariesContainer.getExtraAttributes(), + true)); + forceRewriteOfCPE = true; + } + + // same thing for the dependencies container + if (foundDependenciesContainer == null) { + // add the android dependencies container to the array + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newContainerEntry( + new Path(AdtConstants.CONTAINER_DEPENDENCIES), true)); + } else if (!m2eNature && !foundDependenciesContainer.isExported()) { + // the container is present but it's not exported and since there's no m2e nature + // we do want it to be exported. + // keep all the other parameters the same. + entries = ProjectHelper.replaceEntryInClasspath(entries, + JavaCore.newContainerEntry( + new Path(AdtConstants.CONTAINER_DEPENDENCIES), + foundDependenciesContainer.getAccessRules(), + foundDependenciesContainer.getExtraAttributes(), + true)); + forceRewriteOfCPE = true; + } + + // set the new list of entries to the project + if (entries != oldEntries || forceRewriteOfCPE) { + javaProject.setRawClasspath(entries, new NullProgressMonitor()); + } + + // If needed, check and fix compiler compliance and source compatibility + ProjectHelper.checkAndFixCompilerCompliance(javaProject); + } + + + /** + * Checks the project compiler compliance level is supported. + * @param javaProject The project to check + * @return A pair with the first integer being an error code, and the second value + * being the invalid value found or null. The error code can be: <ul> + * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li> + * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li> + * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li> + * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li> + * </ul> + */ + public static final Pair<Integer, String> checkCompilerCompliance(IJavaProject javaProject) { + // get the project compliance level option + String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true); + + // check it against a list of valid compliance level strings. + if (!checkCompliance(javaProject, compliance)) { + // if we didn't find the proper compliance level, we return an error + return Pair.of(COMPILER_COMPLIANCE_LEVEL, compliance); + } + + // otherwise we check source compatibility + String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true); + + // check it against a list of valid compliance level strings. + if (!checkCompliance(javaProject, source)) { + // if we didn't find the proper compliance level, we return an error + return Pair.of(COMPILER_COMPLIANCE_SOURCE, source); + } + + // otherwise check codegen level + String codeGen = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true); + + // check it against a list of valid compliance level strings. + if (!checkCompliance(javaProject, codeGen)) { + // if we didn't find the proper compliance level, we return an error + return Pair.of(COMPILER_COMPLIANCE_CODEGEN_TARGET, codeGen); + } + + return Pair.of(COMPILER_COMPLIANCE_OK, null); + } + + /** + * Checks the project compiler compliance level is supported. + * @param project The project to check + * @return A pair with the first integer being an error code, and the second value + * being the invalid value found or null. The error code can be: <ul> + * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li> + * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li> + * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li> + * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li> + * </ul> + */ + public static final Pair<Integer, String> checkCompilerCompliance(IProject project) { + // get the java project from the IProject resource object + IJavaProject javaProject = JavaCore.create(project); + + // check and return the result. + return checkCompilerCompliance(javaProject); + } + + + /** + * Checks, and fixes if needed, the compiler compliance level, and the source compatibility + * level + * @param project The project to check and fix. + */ + public static final void checkAndFixCompilerCompliance(IProject project) { + // FIXME This method is never used. Shall we just removed it? + // {@link #checkAndFixCompilerCompliance(IJavaProject)} is used instead. + + // get the java project from the IProject resource object + IJavaProject javaProject = JavaCore.create(project); + + // Now we check the compiler compliance level and make sure it is valid + checkAndFixCompilerCompliance(javaProject); + } + + /** + * Checks, and fixes if needed, the compiler compliance level, and the source compatibility + * level + * @param javaProject The Java project to check and fix. + */ + public static final void checkAndFixCompilerCompliance(IJavaProject javaProject) { + Pair<Integer, String> result = checkCompilerCompliance(javaProject); + if (result.getFirst().intValue() != COMPILER_COMPLIANCE_OK) { + // setup the preferred compiler compliance level. + javaProject.setOption(JavaCore.COMPILER_COMPLIANCE, + AdtConstants.COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOption(JavaCore.COMPILER_SOURCE, + AdtConstants.COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, + AdtConstants.COMPILER_COMPLIANCE_PREFERRED); + + // clean the project to make sure we recompile + try { + javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD, + new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.printErrorToConsole(javaProject.getProject(), + "Project compiler settings changed. Clean your project."); + } + } + } + + /** + * Makes the given project use JDK 6 (or more specifically, + * {@link AdtConstants#COMPILER_COMPLIANCE_PREFERRED} as the compilation + * target, regardless of what the default IDE JDK level is, provided a JRE + * of the given level is installed. + * + * @param javaProject the Java project + * @throws CoreException if the IDE throws an exception setting the compiler + * level + */ + @SuppressWarnings("restriction") // JDT API for setting compliance options + public static void enforcePreferredCompilerCompliance(@NonNull IJavaProject javaProject) + throws CoreException { + String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true); + if (compliance == null || + JavaModelUtil.isVersionLessThan(compliance, COMPILER_COMPLIANCE_PREFERRED)) { + IVMInstallType[] types = JavaRuntime.getVMInstallTypes(); + for (int i = 0; i < types.length; i++) { + IVMInstallType type = types[i]; + IVMInstall[] installs = type.getVMInstalls(); + for (int j = 0; j < installs.length; j++) { + IVMInstall install = installs[j]; + if (install instanceof IVMInstall2) { + IVMInstall2 install2 = (IVMInstall2) install; + // Java version can be 1.6.0, and preferred is 1.6 + if (install2.getJavaVersion().startsWith(COMPILER_COMPLIANCE_PREFERRED)) { + Map<String, String> options = javaProject.getOptions(false); + JavaCore.setComplianceOptions(COMPILER_COMPLIANCE_PREFERRED, options); + JavaModelUtil.setDefaultClassfileOptions(options, + COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOptions(options); + return; + } + } + } + } + } + } + + /** + * Returns a {@link IProject} by its running application name, as it returned by the AVD. + * <p/> + * <var>applicationName</var> will in most case be the package declared in the manifest, but + * can, in some cases, be a custom process name declared in the manifest, in the + * <code>application</code>, <code>activity</code>, <code>receiver</code>, or + * <code>service</code> nodes. + * @param applicationName The application name. + * @return a project or <code>null</code> if no matching project were found. + */ + public static IProject findAndroidProjectByAppName(String applicationName) { + // Get the list of project for the current workspace + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject[] projects = workspace.getRoot().getProjects(); + + // look for a project that matches the packageName of the app + // we're trying to debug + for (IProject p : projects) { + if (p.isOpen()) { + try { + if (p.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + // ignore non android projects + continue; + } + } catch (CoreException e) { + // failed to get the nature? skip project. + continue; + } + + // check that there is indeed a manifest file. + IFile manifestFile = getManifest(p); + if (manifestFile == null) { + // no file? skip this project. + continue; + } + + ManifestData data = AndroidManifestHelper.parseForData(manifestFile); + if (data == null) { + // skip this project. + continue; + } + + String manifestPackage = data.getPackage(); + + if (manifestPackage != null && manifestPackage.equals(applicationName)) { + // this is the project we were looking for! + return p; + } else { + // if the package and application name don't match, + // we look for other possible process names declared in the manifest. + String[] processes = data.getProcesses(); + for (String process : processes) { + if (process.equals(applicationName)) { + return p; + } + } + } + } + } + + return null; + + } + + public static void fixProjectNatureOrder(IProject project) throws CoreException { + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // if the android nature is not the first one, we reorder them + if (AdtConstants.NATURE_DEFAULT.equals(natures[0]) == false) { + // look for the index + for (int i = 0 ; i < natures.length ; i++) { + if (AdtConstants.NATURE_DEFAULT.equals(natures[i])) { + // if we try to just reorder the array in one pass, this doesn't do + // anything. I guess JDT check that we are actually adding/removing nature. + // So, first we'll remove the android nature, and then add it back. + + // remove the android nature + removeNature(project, AdtConstants.NATURE_DEFAULT); + + // now add it back at the first index. + description = project.getDescription(); + natures = description.getNatureIds(); + + String[] newNatures = new String[natures.length + 1]; + + // first one is android + newNatures[0] = AdtConstants.NATURE_DEFAULT; + + // next the rest that was before the android nature + System.arraycopy(natures, 0, newNatures, 1, natures.length); + + // set the new natures + description.setNatureIds(newNatures); + project.setDescription(description, null); + + // and stop + break; + } + } + } + } + + + /** + * Removes a specific nature from a project. + * @param project The project to remove the nature from. + * @param nature The nature id to remove. + * @throws CoreException + */ + public static void removeNature(IProject project, String nature) throws CoreException { + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // check if the project already has the android nature. + for (int i = 0; i < natures.length; ++i) { + if (nature.equals(natures[i])) { + String[] newNatures = new String[natures.length - 1]; + if (i > 0) { + System.arraycopy(natures, 0, newNatures, 0, i); + } + System.arraycopy(natures, i + 1, newNatures, i, natures.length - i - 1); + description.setNatureIds(newNatures); + project.setDescription(description, null); + + return; + } + } + + } + + /** + * Returns if the project has error level markers. + * @param includeReferencedProjects flag to also test the referenced projects. + * @throws CoreException + */ + public static boolean hasError(IProject project, boolean includeReferencedProjects) + throws CoreException { + IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); + if (markers != null && markers.length > 0) { + // the project has marker(s). even though they are "problem" we + // don't know their severity. so we loop on them and figure if they + // are warnings or errors + for (IMarker m : markers) { + int s = m.getAttribute(IMarker.SEVERITY, -1); + if (s == IMarker.SEVERITY_ERROR) { + return true; + } + } + } + + // test the referenced projects if needed. + if (includeReferencedProjects) { + List<IProject> projects = getReferencedProjects(project); + + for (IProject p : projects) { + if (hasError(p, false)) { + return true; + } + } + } + + return false; + } + + /** + * Saves a String property into the persistent storage of a resource. + * @param resource The resource into which the string value is saved. + * @param propertyName the name of the property. The id of the plug-in is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + public static boolean saveStringProperty(IResource resource, String propertyName, + String value) { + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); + + try { + resource.setPersistentProperty(qname, value); + } catch (CoreException e) { + return false; + } + + return true; + } + + /** + * Loads a String property from the persistent storage of a resource. + * @param resource The resource from which the string value is loaded. + * @param propertyName the name of the property. The id of the plug-in is added to this string. + * @return the property value or null if it was not found. + */ + public static String loadStringProperty(IResource resource, String propertyName) { + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); + + try { + String value = resource.getPersistentProperty(qname); + return value; + } catch (CoreException e) { + return null; + } + } + + /** + * Saves a property into the persistent storage of a resource. + * @param resource The resource into which the boolean value is saved. + * @param propertyName the name of the property. The id of the plug-in is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + public static boolean saveBooleanProperty(IResource resource, String propertyName, + boolean value) { + return saveStringProperty(resource, propertyName, Boolean.toString(value)); + } + + /** + * Loads a boolean property from the persistent storage of a resource. + * @param resource The resource from which the boolean value is loaded. + * @param propertyName the name of the property. The id of the plug-in is added to this string. + * @param defaultValue The default value to return if the property was not found. + * @return the property value or the default value if the property was not found. + */ + public static boolean loadBooleanProperty(IResource resource, String propertyName, + boolean defaultValue) { + String value = loadStringProperty(resource, propertyName); + if (value != null) { + return Boolean.parseBoolean(value); + } + + return defaultValue; + } + + public static Boolean loadBooleanProperty(IResource resource, String propertyName) { + String value = loadStringProperty(resource, propertyName); + if (value != null) { + return Boolean.valueOf(value); + } + + return null; + } + + /** + * Saves the path of a resource into the persistent storage of a resource. + * @param resource The resource into which the resource path is saved. + * @param propertyName the name of the property. The id of the plug-in is added to this string. + * @param value The resource to save. It's its path that is actually stored. If null, an + * empty string is stored. + * @return true if the save succeeded + */ + public static boolean saveResourceProperty(IResource resource, String propertyName, + IResource value) { + if (value != null) { + IPath iPath = value.getFullPath(); + return saveStringProperty(resource, propertyName, iPath.toString()); + } + + return saveStringProperty(resource, propertyName, ""); //$NON-NLS-1$ + } + + /** + * Loads the path of a resource from the persistent storage of a resource, and returns the + * corresponding IResource object. + * @param resource The resource from which the resource path is loaded. + * @param propertyName the name of the property. The id of the plug-in is added to this string. + * @return The corresponding IResource object (or children interface) or null + */ + public static IResource loadResourceProperty(IResource resource, String propertyName) { + String value = loadStringProperty(resource, propertyName); + + if (value != null && value.length() > 0) { + return ResourcesPlugin.getWorkspace().getRoot().findMember(new Path(value)); + } + + return null; + } + + /** + * Returns the list of referenced project that are opened and Java projects. + * @param project + * @return a new list object containing the opened referenced java project. + * @throws CoreException + */ + public static List<IProject> getReferencedProjects(IProject project) throws CoreException { + IProject[] projects = project.getReferencedProjects(); + + ArrayList<IProject> list = new ArrayList<IProject>(); + + for (IProject p : projects) { + if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { + list.add(p); + } + } + + return list; + } + + + /** + * Checks a Java project compiler level option against a list of supported versions. + * @param optionValue the Compiler level option. + * @return true if the option value is supported. + */ + private static boolean checkCompliance(@NonNull IJavaProject project, String optionValue) { + for (String s : AdtConstants.COMPILER_COMPLIANCE) { + if (s != null && s.equals(optionValue)) { + return true; + } + } + + if (JavaCore.VERSION_1_7.equals(optionValue)) { + // Requires API 19 and buildTools 19 + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IProject p = project.getProject(); + IAndroidTarget target = currentSdk.getTarget(p); + if (target == null || target.getVersion().getApiLevel() < 19) { + return false; + } + + ProjectState projectState = Sdk.getProjectState(p); + if (projectState != null) { + BuildToolInfo buildToolInfo = projectState.getBuildToolInfo(); + if (buildToolInfo == null) { + buildToolInfo = currentSdk.getLatestBuildTool(); + } + if (buildToolInfo == null || buildToolInfo.getRevision().getMajor() < 19) { + return false; + } + } + + return true; + } + } + + return false; + } + + /** + * Returns the apk filename for the given project + * @param project The project. + * @param config An optional config name. Can be null. + */ + public static String getApkFilename(IProject project, String config) { + if (config != null) { + return project.getName() + "-" + config + SdkConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$ + } + + return project.getName() + SdkConstants.DOT_ANDROID_PACKAGE; + } + + /** + * Find the list of projects on which this JavaProject is dependent on at the compilation level. + * + * @param javaProject Java project that we are looking for the dependencies. + * @return A list of Java projects for which javaProject depend on. + * @throws JavaModelException + */ + public static List<IJavaProject> getAndroidProjectDependencies(IJavaProject javaProject) + throws JavaModelException { + String[] requiredProjectNames = javaProject.getRequiredProjectNames(); + + // Go from java project name to JavaProject name + IJavaModel javaModel = javaProject.getJavaModel(); + + // loop through all dependent projects and keep only those that are Android projects + List<IJavaProject> projectList = new ArrayList<IJavaProject>(requiredProjectNames.length); + for (String javaProjectName : requiredProjectNames) { + IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName); + + //Verify that the project has also the Android Nature + try { + if (!androidJavaProject.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) { + continue; + } + } catch (CoreException e) { + continue; + } + + projectList.add(androidJavaProject); + } + + return projectList; + } + + /** + * Returns the android package file as an IFile object for the specified + * project. + * @param project The project + * @return The android package as an IFile object or null if not found. + */ + public static IFile getApplicationPackage(IProject project) { + // get the output folder + IFolder outputLocation = BaseProjectHelper.getAndroidOutputFolder(project); + + if (outputLocation == null) { + AdtPlugin.printErrorToConsole(project, + "Failed to get the output location of the project. Check build path properties" + ); + return null; + } + + + // get the package path + String packageName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE; + IResource r = outputLocation.findMember(packageName); + + // check the package is present + if (r instanceof IFile && r.exists()) { + return (IFile)r; + } + + String msg = String.format("Could not find %1$s!", packageName); + AdtPlugin.printErrorToConsole(project, msg); + + return null; + } + + /** + * Returns an {@link IFile} object representing the manifest for the given project. + * + * @param project The project containing the manifest file. + * @return An IFile object pointing to the manifest or null if the manifest + * is missing. + */ + public static IFile getManifest(IProject project) { + IResource r = project.findMember(AdtConstants.WS_SEP + + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (r == null || r.exists() == false || (r instanceof IFile) == false) { + return null; + } + return (IFile) r; + } + + /** + * Does a full release build of the application, including the libraries. Do not build the + * package. + * + * @param project The project to be built. + * @param monitor A eclipse runtime progress monitor to be updated by the builders. + * @throws CoreException + */ + @SuppressWarnings("unchecked") + public static void compileInReleaseMode(IProject project, IProgressMonitor monitor) + throws CoreException { + compileInReleaseMode(project, true /*includeDependencies*/, monitor); + } + + /** + * Does a full release build of the application, including the libraries. Do not build the + * package. + * + * @param project The project to be built. + * @param monitor A eclipse runtime progress monitor to be updated by the builders. + * @throws CoreException + */ + @SuppressWarnings("unchecked") + private static void compileInReleaseMode(IProject project, boolean includeDependencies, + IProgressMonitor monitor) + throws CoreException { + + if (includeDependencies) { + ProjectState projectState = Sdk.getProjectState(project); + + // this gives us all the library projects, direct and indirect dependencies, + // so no need to run this method recursively. + List<IProject> libraries = projectState.getFullLibraryProjects(); + + // build dependencies in reverse order to prevent libraries being rebuilt + // due to refresh of other libraries (they would be compiled in the wrong mode). + for (int i = libraries.size() - 1 ; i >= 0 ; i--) { + IProject lib = libraries.get(i); + compileInReleaseMode(lib, false /*includeDependencies*/, monitor); + + // force refresh of the dependency. + lib.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + } + + // do a full build on all the builders to guarantee that the builders are called. + // (Eclipse does an optimization where builders are not called if there aren't any + // deltas). + + ICommand[] commands = project.getDescription().getBuildSpec(); + for (ICommand command : commands) { + String name = command.getBuilderName(); + if (PreCompilerBuilder.ID.equals(name)) { + Map newArgs = new HashMap(); + newArgs.put(PreCompilerBuilder.RELEASE_REQUESTED, ""); + if (command.getArguments() != null) { + newArgs.putAll(command.getArguments()); + } + + project.build(IncrementalProjectBuilder.FULL_BUILD, + PreCompilerBuilder.ID, newArgs, monitor); + } else if (PostCompilerBuilder.ID.equals(name)) { + if (includeDependencies == false) { + // this is a library, we need to build it! + project.build(IncrementalProjectBuilder.FULL_BUILD, name, + command.getArguments(), monitor); + } + } else { + + project.build(IncrementalProjectBuilder.FULL_BUILD, name, + command.getArguments(), monitor); + } + } + } + + /** + * Force building the project and all its dependencies. + * + * @param project the project to build + * @param kind the build kind + * @param monitor + * @throws CoreException + */ + public static void buildWithDeps(IProject project, int kind, IProgressMonitor monitor) + throws CoreException { + // Get list of projects that we depend on + ProjectState projectState = Sdk.getProjectState(project); + + // this gives us all the library projects, direct and indirect dependencies, + // so no need to run this method recursively. + List<IProject> libraries = projectState.getFullLibraryProjects(); + + // build dependencies in reverse order to prevent libraries being rebuilt + // due to refresh of other libraries (they would be compiled in the wrong mode). + for (int i = libraries.size() - 1 ; i >= 0 ; i--) { + IProject lib = libraries.get(i); + lib.build(kind, monitor); + lib.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + + project.build(kind, monitor); + } + + + /** + * Build project incrementally, including making the final packaging even if it is disabled + * by default. + * + * @param project The project to be built. + * @param monitor A eclipse runtime progress monitor to be updated by the builders. + * @throws CoreException + */ + public static void doFullIncrementalDebugBuild(IProject project, IProgressMonitor monitor) + throws CoreException { + // Get list of projects that we depend on + List<IJavaProject> androidProjectList = new ArrayList<IJavaProject>(); + try { + androidProjectList = getAndroidProjectDependencies( + BaseProjectHelper.getJavaProject(project)); + } catch (JavaModelException e) { + AdtPlugin.printErrorToConsole(project, e); + } + // Recursively build dependencies + for (IJavaProject dependency : androidProjectList) { + doFullIncrementalDebugBuild(dependency.getProject(), monitor); + } + + // Do an incremental build to pick up all the deltas + project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor); + + // If the preferences indicate not to use post compiler optimization + // then the incremental build will have done everything necessary, otherwise, + // we have to run the final builder manually (if requested). + if (AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) { + // Create the map to pass to the PostC builder + Map<String, String> args = new TreeMap<String, String>(); + args.put(PostCompilerBuilder.POST_C_REQUESTED, ""); //$NON-NLS-1$ + + // call the post compiler manually, forcing FULL_BUILD otherwise Eclipse won't + // call the builder since the delta is empty. + project.build(IncrementalProjectBuilder.FULL_BUILD, + PostCompilerBuilder.ID, args, monitor); + } + + // because the post compiler builder does a delayed refresh due to + // library not picking the refresh up if it's done during the build, + // we want to force a refresh here as this call is generally asking for + // a build to use the apk right after the call. + project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/SupportLibraryHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/SupportLibraryHelper.java new file mode 100644 index 000000000..e1819b283 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/SupportLibraryHelper.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 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.project; + +import static com.android.SdkConstants.FQCN_GRID_LAYOUT; +import static com.android.SdkConstants.FQCN_GRID_LAYOUT_V7; +import static com.android.SdkConstants.FQCN_SPACE; +import static com.android.SdkConstants.FQCN_SPACE_V7; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; + +/** + * Helper class for the Android Support Library. The support library provides + * (for example) a backport of GridLayout, which must be used as a library + * project rather than a jar library since it has resources. This class provides + * support for finding the library project, or downloading and installing it on + * demand if it does not, as well as translating tags such as + * {@code <GridLayout>} into {@code <com.android.support.v7.GridLayout>} if it + * does not. + */ +public class SupportLibraryHelper { + /** + * Returns the correct tag to use for the given view tag. This is normally + * the same as the tag itself. However, for some views which are not available + * on all platforms, this will: + * <ul> + * <li> Check if the view is available in the compatibility library, + * and if so, if the support library is not installed, will offer to + * install it via the SDK manager. + * <li> (The tool may also offer to adjust the minimum SDK of the project + * up to a level such that the given tag is supported directly, and then + * this method will return the original tag.) + * <li> Check whether the compatibility library is included in the project, and + * if not, offer to copy it into the workspace and add a library dependency. + * <li> Return the alternative tag. For example, for "GridLayout", it will + * (if the minimum SDK is less than 14) return "com.android.support.v7.GridLayout" + * instead. + * </ul> + * + * @param project the project to add the dependency into + * @param tag the tag to look up, such as "GridLayout" + * @return the tag to use in the layout, normally the same as the input tag but possibly + * an equivalent compatibility library tag instead. + */ + @NonNull + public static String getTagFor(@NonNull IProject project, @NonNull String tag) { + boolean isGridLayout = tag.equals(FQCN_GRID_LAYOUT); + boolean isSpace = tag.equals(FQCN_SPACE); + if (isGridLayout || isSpace) { + int minSdk = ManifestInfo.get(project).getMinSdkVersion(); + if (minSdk < 14) { + // See if the support library is installed in the SDK area + // See if there is a local project in the workspace providing the + // project + IProject supportProject = getSupportProjectV7(); + if (supportProject != null) { + // Make sure I have a dependency on it + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + for (LibraryState library : state.getLibraries()) { + if (supportProject.equals(library.getProjectState().getProject())) { + // Found it: you have the compatibility library and have linked + // to it: use the alternative tag + return isGridLayout ? FQCN_GRID_LAYOUT_V7 : FQCN_SPACE_V7; + } + } + } + } + + // Ask user to install it + String message = String.format( + "%1$s requires API level 14 or higher, or a compatibility " + + "library for older versions.\n\n" + + " Do you want to install the compatibility library?", tag); + MessageDialog dialog = + new MessageDialog( + Display.getCurrent().getActiveShell(), + "Warning", + null, + message, + MessageDialog.QUESTION, + new String[] { + "Install", "Cancel" + }, + 1 /* default button: Cancel */); + int answer = dialog.open(); + if (answer == 0) { + if (supportProject != null) { + // Just add library dependency + if (!AddSupportJarAction.addLibraryDependency( + supportProject, + project, + true /* waitForFinish */)) { + return tag; + } + } else { + // Install library AND add dependency + if (!AddSupportJarAction.installGridLayoutLibrary( + project, + true /* waitForFinish */)) { + return tag; + } + } + + return isGridLayout ? FQCN_GRID_LAYOUT_V7 : FQCN_SPACE_V7; + } + } + } + + return tag; + } + + /** Cache for {@link #getSupportProjectV7()} */ + private static IProject sCachedProject; + + /** + * Finds and returns the support project in the workspace, if any. + * + * @return the android support library project, or null if not found + */ + @Nullable + public static IProject getSupportProjectV7() { + if (sCachedProject != null) { + if (sCachedProject.isAccessible()) { + return sCachedProject; + } else { + sCachedProject = null; + } + } + + sCachedProject = findSupportProjectV7(); + return sCachedProject; + } + + @Nullable + private static IProject findSupportProjectV7() { + for (IJavaProject javaProject : AdtUtils.getOpenAndroidProjects()) { + IProject project = javaProject.getProject(); + ProjectState state = Sdk.getProjectState(project); + if (state != null && state.isLibrary()) { + ManifestInfo manifestInfo = ManifestInfo.get(project); + if (manifestInfo.getPackage().equals("android.support.v7.gridlayout")) { //$NON-NLS-1$ + return project; + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/XmlErrorHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/XmlErrorHandler.java new file mode 100644 index 000000000..c496c7e57 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/XmlErrorHandler.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.ide.common.xml.AndroidManifestParser.ManifestErrorHandler; +import com.android.ide.eclipse.adt.AdtConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaProject; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * XML error handler used by the parser to report errors/warnings. + */ +public class XmlErrorHandler extends DefaultHandler implements ManifestErrorHandler { + + private final IJavaProject mJavaProject; + /** file being parsed */ + private final IFile mFile; + /** link to the delta visitor, to set the xml error flag */ + private final XmlErrorListener mErrorListener; + + /** + * Classes which implement this interface provide a method that deals + * with XML errors. + */ + public interface XmlErrorListener { + /** + * Sent when an XML error is detected. + */ + public void errorFound(); + } + + public static class BasicXmlErrorListener implements XmlErrorListener { + public boolean mHasXmlError = false; + + @Override + public void errorFound() { + mHasXmlError = true; + } + } + + public XmlErrorHandler(IJavaProject javaProject, IFile file, XmlErrorListener errorListener) { + mJavaProject = javaProject; + mFile = file; + mErrorListener = errorListener; + } + + public XmlErrorHandler(IFile file, XmlErrorListener errorListener) { + this(null, file, errorListener); + } + + /** + * Xml Error call back + * @param exception the parsing exception + * @throws SAXException + */ + @Override + public void error(SAXParseException exception) throws SAXException { + handleError(exception, exception.getLineNumber()); + } + + /** + * Xml Fatal Error call back + * @param exception the parsing exception + * @throws SAXException + */ + @Override + public void fatalError(SAXParseException exception) throws SAXException { + handleError(exception, exception.getLineNumber()); + } + + /** + * Xml Warning call back + * @param exception the parsing exception + * @throws SAXException + */ + @Override + public void warning(SAXParseException exception) throws SAXException { + if (mFile != null) { + BaseProjectHelper.markResource(mFile, + AdtConstants.MARKER_XML, + exception.getMessage(), + exception.getLineNumber(), + IMarker.SEVERITY_WARNING); + } + } + + protected final IFile getFile() { + return mFile; + } + + /** + * Handles a parsing error and an optional line number. + * @param exception + * @param lineNumber + */ + @Override + public void handleError(Exception exception, int lineNumber) { + if (mErrorListener != null) { + mErrorListener.errorFound(); + } + + String message = exception.getMessage(); + if (message == null) { + message = "Unknown error " + exception.getClass().getCanonicalName(); + } + + if (mFile != null) { + BaseProjectHelper.markResource(mFile, + AdtConstants.MARKER_XML, + message, + lineNumber, + IMarker.SEVERITY_ERROR); + } + } + + /** + * Checks that a class is valid and can be used in the Android Manifest. + * <p/> + * Errors are put as {@link IMarker} on the manifest file. + * @param locator + * @param className the fully qualified name of the class to test. + * @param superClassName the fully qualified name of the class it is supposed to extend. + * @param testVisibility if <code>true</code>, the method will check the visibility of + * the class or of its constructors. + */ + @Override + public void checkClass(Locator locator, String className, String superClassName, + boolean testVisibility) { + if (mJavaProject == null) { + return; + } + // we need to check the validity of the activity. + String result = BaseProjectHelper.testClassForManifest(mJavaProject, + className, superClassName, testVisibility); + if (result != BaseProjectHelper.TEST_CLASS_OK) { + // get the line number + int line = locator.getLineNumber(); + + // mark the file + IMarker marker = BaseProjectHelper.markResource(getFile(), + AdtConstants.MARKER_ANDROID, result, line, IMarker.SEVERITY_ERROR); + + // add custom attributes to be used by the manifest editor. + if (marker != null) { + try { + marker.setAttribute(AdtConstants.MARKER_ATTR_TYPE, + AdtConstants.MARKER_ATTR_TYPE_ACTIVITY); + marker.setAttribute(AdtConstants.MARKER_ATTR_CLASS, className); + } catch (CoreException e) { + } + } + } + } +} |