/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.tools.idea.gradle.eclipse; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.sdklib.AndroidTargetHash; import com.android.sdklib.AndroidVersion; import com.android.sdklib.SdkVersionInfo; import com.android.tools.idea.gradle.util.PropertiesUtil; import com.android.tools.lint.detector.api.LintUtils; import com.android.utils.SdkUtils; import com.android.utils.XmlUtils; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.Files; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.android.SdkConstants.*; import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK; import static com.android.tools.idea.gradle.eclipse.ImportModule.*; import static com.android.utils.SdkUtils.endsWithIgnoreCase; import static com.android.xml.AndroidManifest.*; import static java.io.File.separator; import static java.io.File.separatorChar; /** Provides information about an Eclipse project */ class EclipseProject implements Comparable<EclipseProject> { static final String DEFAULT_LANGUAGE_LEVEL = "1.6"; 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 GradleImport myImporter; private final File myDir; private final File myCanonicalDir; private boolean myLibrary; private boolean myAndroidProject; private boolean myNdkProject; private AndroidVersion myMinSdkVersion; private AndroidVersion myTargetSdkVersion; private Document myProjectDoc; private Document myManifestDoc; private Properties myProjectProperties; private AndroidVersion myVersion; private String myName; private String myLanguageLevel; private List<EclipseProject> myDirectLibraries; private List<File> mySourcePaths; private List<File> myJarPaths; private List<File> myInstrumentationJarPaths; private List<File> myNativeLibs; private List<String> myInferredLibraries; private File myNativeSources; private String myNativeModuleName; private File myOutputDir; private String myPackage; private List<File> myLocalProguardFiles; private List<File> mySdkProguardFiles; private List<EclipseProject> myAllLibraries; private EclipseImportModule myModule; private Map<String, String> myProjectVariableMap; private Map<String, String> myLinkedResourceMap; private File myInstrumentationDir; private Map<File, Charset> myFileCharsets; private Charset myDefaultCharset; private EclipseProject(@NonNull GradleImport importer, @NonNull File dir) throws IOException { myImporter = importer; myDir = dir; myCanonicalDir = dir.getCanonicalFile(); // Ensure that the library references (which are canonicalized) find this project // if included from multiple locations myImporter.registerProject(this); initProjectName(); initAndroidProject(); initLanguageLevel(); if (isAndroidProject()) { Properties properties = getProjectProperties(); if (properties != null) { initProguard(properties); initVersion(properties); initLibraries(properties); initLibrary(properties); } initPackage(); initMinSdkVersion(); initInstrumentation(); } else { myDirectLibraries = new ArrayList<EclipseProject>(4); } initClassPathEntries(); initJni(); initEncoding(); } @NonNull public static EclipseProject getProject(@NonNull GradleImport importer, @NonNull File dir) throws IOException { Map<File, EclipseProject> mProjectMap = importer.getProjectMap(); EclipseProject project = mProjectMap.get(dir); if (project == null) { project = createProject(importer, dir); // The project should register itself in the map; we don't have to do that here. // (The code used to do that here, but it turns out project creation can recursively // visit library references as part of initialization, so have the projects register // themselves prior to initialization instead) assert mProjectMap.get(dir) != null; } return project; } @NonNull private static EclipseProject createProject(@NonNull GradleImport importer, @NonNull File dir) throws IOException { // Read the .classpath, .project, project.properties and local.properties files (if there) return new EclipseProject(importer, dir); } @Nullable private static AndroidVersion getApiVersion(Element usesSdk, String attribute, @Nullable AndroidVersion defaultApiLevel) { String valueString = null; if (usesSdk.hasAttributeNS(ANDROID_URI, attribute)) { valueString = usesSdk.getAttributeNS(ANDROID_URI, attribute); } if (valueString != null) { AndroidVersion version = SdkVersionInfo.getVersion(valueString, null); if (version != null) { return version; } } return defaultApiLevel; } @Nullable private static String getInstrumentationTarget(@NonNull GradleImport importer, @NonNull File manifest) throws IOException { Document doc = importer.getXmlDocument(manifest, true); if (doc != null) { NodeList list = doc.getElementsByTagName(NODE_INSTRUMENTATION); for (int i = 0; i < list.getLength(); i++) { Element tag = (Element)list.item(i); String target = tag.getAttributeNS(ANDROID_URI, ATTRIBUTE_TARGET_PACKAGE); if (target != null && !target.isEmpty()) { return target; } } } return null; } @Nullable private static String getStringValue(@NonNull Element element) { NodeList children = element.getChildNodes(); for (int j = 0; j < children.getLength(); j++) { Node child = children.item(j); if (child.getNodeType() == Node.TEXT_NODE) { return child.getNodeValue().trim(); } } return null; } /** * Creates a list of modules from the given set of projects. The returned list * is in dependency order. */ public static List<? extends ImportModule> performImport(@NonNull GradleImport importer, @NonNull Collection<EclipseProject> projects) { List<EclipseImportModule> modules = Lists.newArrayList(); List<EclipseImportModule> replacedByDependencies = Lists.newArrayList(); for (EclipseProject project : projects) { EclipseImportModule module = new EclipseImportModule(importer, project); module.initialize(); if (module.isReplacedWithDependency()) { replacedByDependencies.add(module); } else { modules.add(module); } } // Some libraries may be replaced by just a dependency (for example, // instead of copying in a whole copy of ActionBarSherlock, just // replace by the corresponding dependency. for (EclipseImportModule replaced : replacedByDependencies) { assert replaced.getReplaceWithDependencies() != null; EclipseProject project = replaced.getProject(); for (EclipseImportModule module : modules) { if (module.getProject().getAllLibraries().contains(project)) { module.addDependencies(replaced.getReplaceWithDependencies()); } } } // Strip out .jar files from the libs/ folder if already implied by // library dependencies for (EclipseImportModule module : modules) { module.removeJarDependencies(); } // Sort by dependency order Collections.sort(modules); return modules; } private void initVersion(@NonNull Properties properties) { String target = properties.getProperty("target"); //$NON-NLS-1$ if (target != null) { myVersion = AndroidTargetHash.getPlatformVersion(target); // getPlatformVersion does not handle API numbers correctly if (myVersion != null && myVersion.isPreview()) { // Update codename AndroidVersion version = SdkVersionInfo.getVersion(myVersion.getCodename(), null); if (version != null) { myVersion = version; } } } } private void initLibraries(@NonNull Properties properties) throws IOException { myDirectLibraries = new ArrayList<EclipseProject>(4); for (int i = 0; i < 1000; i++) { String key = String.format(ANDROID_LIBRARY_REFERENCE_FORMAT, i); String library = properties.getProperty(key); if (library == null || library.isEmpty()) { // No holes in the numbering sequence is allowed if (i == 0) { // Except for i=0; library projects are supposed to start with 1, and // all the ADT, sdklib and ant code which reads and writes these start with // 1, but I've encountered several projects in the wild that start with 0; // presumably from manual edits or because some older version of the tools // did this. // Instead of bailing here, try 1 too. continue; } else { // After 1, we don't allow any gaps in the sequence break; } } // Handle importing Windows-relative paths in project.properties on non-Windows, // and vice versa if (CURRENT_PLATFORM == PLATFORM_WINDOWS) { library = library.replace('/', '\\'); } else { library = library.replace('\\', '/'); } File path = new File(library); File joined = path.isAbsolute() ? path : new File(myDir, library); File libraryDir = joined.getCanonicalFile(); if (!libraryDir.exists()) { if (myImporter.isReplaceLibs()) { // Look for some common libraries that we can probably just guess as a dependency replacement String libraryDirName = libraryDir.getName().replace('_', '-'); if (libraryDirName.indexOf('-') == -1) { File parent = libraryDir.getParentFile(); if (parent != null) { String parentName = parent.getName(); if (parentName.equals("v7")) { // Recognize paths pointing into an SDK extras folder such as // ../../../android-sdk-mac_86/extras/android/compatibility/v7/appcompat // and turn them into appcompat-v7 such that they match the artifact check below libraryDirName = libraryDirName + '-' + parentName; } } } if (APPCOMPAT_ARTIFACT.equals(libraryDirName) || SUPPORT_ARTIFACT.equals(libraryDirName) || GRIDLAYOUT_ARTIFACT.equals(libraryDirName) || MEDIA_ROUTER_ARTIFACT.equals(libraryDirName)) { if (myInferredLibraries == null) { myInferredLibraries = Lists.newArrayList(); } myInferredLibraries.add(libraryDirName); continue; } } String message = "Library reference " + library + " could not be found"; if (!path.isAbsolute()) { message += "\nPath is " + joined + " which resolves to " + libraryDir.getPath(); } myImporter.reportError(this, getProjectPropertiesFile(), message); } EclipseProject libraryPrj = getProject(myImporter, libraryDir); myDirectLibraries.add(libraryPrj); } } private void initLibrary(@NonNull Properties properties) throws IOException { // This initialization must run after we've initialized the set of library // projects so we know whether or not we're including/merging manifests assert myDirectLibraries != null; String value = properties.getProperty(ANDROID_LIBRARY); myLibrary = VALUE_TRUE.equals(value); if (!myLibrary) { boolean mergeManifests = VALUE_TRUE.equals(properties.getProperty("manifestmerger.enabled")); //$NON-NLS-1$ if (!mergeManifests) { // See if we (transitively) depend on libraries, and if any of them are // android library projects with non-empty manifests for (EclipseProject library : getAllLibraries()) { if (library.isAndroidProject() && library.isLibrary() && library.getManifestFile().exists() && library.getManifestDoc().getDocumentElement() != null && XmlUtils.hasElementChildren(library.getManifestDoc(). getDocumentElement())) { myImporter.getSummary().reportManifestsMayDiffer(); break; } } } } } private void initPackage() throws IOException { myPackage = getManifestDoc().getDocumentElement().getAttribute(ATTR_PACKAGE); } private void initMinSdkVersion() throws IOException { NodeList usesSdks = getManifestDoc().getDocumentElement().getElementsByTagName(NODE_USES_SDK); if (usesSdks.getLength() > 0) { Element usesSdk = (Element)usesSdks.item(0); myMinSdkVersion = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, AndroidVersion.DEFAULT); myTargetSdkVersion = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, myMinSdkVersion); } else { myMinSdkVersion = null; myTargetSdkVersion = null; } } private void initProjectName() throws IOException { Document document = getProjectDocument(); if (document == null) { return; } NodeList names = document.getElementsByTagName("name"); for (int i = 0; i < names.getLength(); i++) { Node element = names.item(i); myName = getStringValue((Element)element); //noinspection VariableNotUsedInsideIf if (myName != null) { break; } } if (myName == null) { myName = myDir.getName(); } } private void initJni() throws IOException { File jniDir = new File(myDir, "jni"); if (!jniDir.exists()) { return; } //noinspection SpellCheckingInspection if (myNdkProject) { myNativeSources = jniDir; File makefile = new File(jniDir, "Android.mk"); if (makefile.exists()) { Pattern pattern = Pattern.compile("\\s*LOCAL_MODULE\\s*:=\\s*(\\S+)\\s*"); for (String line : Files.readLines(makefile, Charsets.UTF_8)) { Matcher matcher = pattern.matcher(line); if (matcher.matches()) { myNativeModuleName = matcher.group(1); if (myNativeLibs != null) { // Remove libs from the libs/<abi> folder if they are just // outputs from these sources String libName = "lib" + myNativeModuleName + ".so"; ListIterator<File> iterator = myNativeLibs.listIterator(); while (iterator.hasNext()) { File lib = iterator.next(); if (libName.equals(lib.getName())) { iterator.remove(); } } if (myNativeLibs.isEmpty()) { myNativeLibs = null; } } break; } } } } } private void initEncoding() throws IOException { File propertyFile = new File(myDir, ".settings" + separator + "org.eclipse.core.resources.prefs"); if (propertyFile.exists()) { Properties properties = PropertiesUtil.getProperties(propertyFile); if (properties != null) { Enumeration<?> enumeration = properties.propertyNames(); String prefix = "encoding/"; while (enumeration.hasMoreElements()) { Object next = enumeration.nextElement(); if (next instanceof String) { String key = (String)next; if (key.startsWith(prefix)) { String path = key.substring(prefix.length()); String encoding = properties.getProperty(key); if (encoding != null && !encoding.isEmpty()) { try { Charset charset = Charset.forName(encoding); if ("<project>".equals(path)) { myDefaultCharset = charset; } else { if (myFileCharsets == null) { myFileCharsets = Maps.newHashMap(); } File file = resolveVariableExpression(path); if (file != null) { myFileCharsets.put(file, charset); } else { myFileCharsets.put(new File(path), charset); } } } catch (UnsupportedCharsetException uce) { myImporter.reportWarning(this, propertyFile, "Unknown charset " + encoding); } } } } } } } } @Nullable Charset getProjectEncoding() { return myDefaultCharset; } @Nullable Charset getFileEncoding(@NonNull File file) { return myFileCharsets != null ? myFileCharsets.get(file) : null; } private void initInstrumentation() throws IOException { // Find unit test projects pointing to this Gradle project. Where do we look? // For now, in direct sub directories of the project, as well as sibling directories File projectDir = findInstrumentationTests(myDir); if (projectDir == null && myDir.getParentFile() != null) { projectDir = findInstrumentationTests(myDir.getParentFile()); } if (projectDir != null && !projectDir.equals(myDir)) { myInstrumentationDir = projectDir; File libs = new File(myInstrumentationDir, LIBS_FOLDER); if (libs.exists()) { File[] files = libs.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && endsWithIgnoreCase(file.getPath(), DOT_JAR)) { if (myInstrumentationJarPaths == null) { myInstrumentationJarPaths = Lists.newArrayList(); } myInstrumentationJarPaths.add(file); } } } } } } @Nullable private File findInstrumentationTests(File parent) { File[] files = parent.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { File manifest = new File(file, ANDROID_MANIFEST_XML); if (manifest.exists()) { try { String target = getInstrumentationTarget(myImporter, manifest); if (target != null && target.equals(myPackage)) { return file; } } catch (IOException e) { // Ignore this manifest } } } } } return null; } private void initClassPathEntries() throws IOException { assert mySourcePaths == null && myJarPaths == null; mySourcePaths = Lists.newArrayList(); myJarPaths = Lists.newArrayList(); Document document = null; File classPathFile = getClassPathFile(); if (!classPathFile.exists()) { File src = new File(myDir, FD_SOURCES); if (src.exists()) { mySourcePaths.add(src); } } else { document = myImporter.getXmlDocument(classPathFile, false); } if (document != null) { NodeList entries = document.getElementsByTagName("classpathentry"); for (int i = 0; i < entries.getLength(); i++) { Node entry = entries.item(i); assert entry.getNodeType() == Node.ELEMENT_NODE; Element element = (Element)entry; String kind = element.getAttribute("kind"); String path = element.getAttribute("path"); if (kind.equals("var")) { File resolved = resolveVariableExpression(path); if (resolved != null) { mySourcePaths.add(resolved); } else { myImporter.reportWarning(this, getClassPathFile(), "Could not resolve path variable " + path); } } else if (kind.equals("src") && !path.isEmpty()) { if (!path.equals(GEN_FOLDER)) { // ignore special generated source folder File resolved = resolveVariableExpression(path); if (resolved != null) { if (path.startsWith("/") && GradleImport.isEclipseProjectDir(resolved)) { // It's pointing to another project. Just add a dependency. EclipseProject lib = getProject(myImporter, resolved); if (!myDirectLibraries.contains(lib)) { myDirectLibraries.add(lib); myAllLibraries = null; // force refresh if already consulted } } else { // It's some other source directory: just include as a source path mySourcePaths.add(resolved); } } else { myImporter.reportWarning(this, getClassPathFile(), "Could not resolve source path " + path + " in project " + getName() + ": ignored. The project may not " + "compile if the given source path provided " + "source code."); } } } else if (kind.equals("lib") && !path.isEmpty()) { // Java library dependency. In Android projects we don't need these since // we pick up the information from the project.properties file for library // dependencies and the libs/ folder for jar files. if (!isAndroidProject()) { File resolved = resolveVariableExpression(path); if (resolved != null) { myJarPaths.add(resolved); } else { myImporter.reportWarning(this, getClassPathFile(), "Absolute path in the path entry: If outside project, may not " + "work correctly: " + path); } } } else if (kind.equals("output") && !path.isEmpty()) { String relative = path.replace('/', separatorChar); File file = new File(relative); if (!file.isAbsolute()) { myOutputDir = file; } } // else: ignore kind="con" } } // Automatically add in libraries in libs File[] libs = new File(myDir, LIBS_FOLDER).listFiles(); if (libs != null) { for (File lib : libs) { if (!lib.isFile()) { // ABI folder? File[] libraries = lib.listFiles(); if (libraries != null) { for (File library : libraries) { String name = library.getName(); if (library.isFile() && name.startsWith("lib") && name.contains(".so")) { // or .endsWith? Allow libfoo.so.1 ? if (myNativeLibs == null) { myNativeLibs = Lists.newArrayList(); } File relative = new File(LIBS_FOLDER, lib.getName() + separator + library.getName()); myNativeLibs.add(relative); } } } continue; } //noinspection ConstantConditions assert lib.isFile(); if (!endsWithIgnoreCase(lib.getPath(), DOT_JAR)) { continue; } File relative = new File(LIBS_FOLDER, lib.getName()); if (!(myJarPaths.contains(relative) || myJarPaths.contains(lib))) { // Skip jars that are the result of a library project dependency boolean isLibraryJar = false; for (EclipseProject project : getAllLibraries()) { if (!project.isAndroidProject()) { continue; } String pkg = project.getPackage(); if (pkg != null) { String jarName = pkg.replace('.', '-') + DOT_JAR; if (jarName.equals(lib.getName())) { isLibraryJar = true; break; } } } if (!isLibraryJar) { myJarPaths.add(relative); } } } Collections.sort(myJarPaths); if (myNativeLibs != null) { Collections.sort(myNativeLibs); } } } private Map<String, String> getProjectVariableMap() { if (myProjectVariableMap == null) { myProjectVariableMap = Maps.newHashMap(); Document document; try { document = getProjectDocument(); if (document == null) { return myProjectVariableMap; } } catch (IOException e) { return myProjectVariableMap; } NodeList variables = document.getElementsByTagName("variable"); for (int i = 0, n = variables.getLength(); i < n; i++) { Element variable = (Element)variables.item(i); NodeList names = variable.getElementsByTagName("name"); NodeList values = variable.getElementsByTagName("value"); if (names.getLength() == 1 && values.getLength() == 1) { String value = getStringValue((Element)values.item(0)); String key = getStringValue((Element)names.item(0)); myProjectVariableMap.put(key, value); } } } return myProjectVariableMap; } private Map<String, String> getLinkedResourceMap() { if (myLinkedResourceMap == null) { myLinkedResourceMap = Maps.newHashMap(); Document document; try { document = getProjectDocument(); if (document == null) { return myProjectVariableMap; } } catch (IOException e) { return myLinkedResourceMap; } NodeList links = document.getElementsByTagName("link"); for (int i = 0, n = links.getLength(); i < n; i++) { Element variable = (Element)links.item(i); NodeList names = variable.getElementsByTagName("name"); NodeList values = variable.getElementsByTagName("locationURI"); if (names.getLength() == 1 && values.getLength() == 1) { String value = getStringValue((Element)values.item(0)); String key = getStringValue((Element)names.item(0)); myLinkedResourceMap.put(key, value); } } } return myLinkedResourceMap; } @VisibleForTesting @Nullable File resolveVariableExpression(@NonNull String path) throws IOException { File file = resolveVariableExpression(path, true, 0); if (file != null && myImporter.getPathMap().containsKey(path)) { myImporter.getPathMap().put(path, file); } return file; } @Nullable private File resolveVariableExpression(@NonNull String path, boolean record, int depth) throws IOException { if (depth > 50) { // probably cyclical definition of variables return null; } if (path.equals("PROJECT_LOC")) { return myDir; } else if (path.equals("PARENT_LOC")) { return myDir.getParentFile(); } else if (path.equals("WORKSPACE_LOC")) { return myImporter.getEclipseWorkspace(); } else if (path.startsWith("PARENT-")) { Pattern pattern = Pattern.compile("PARENT-(\\d+)-(.+)"); Matcher matcher = pattern.matcher(path); if (matcher.matches()) { // Replace suffix a given number of times int count = Integer.parseInt(matcher.group(1)); String target = matcher.group(2); int index = target.indexOf('/'); if (index == -1) { index = target.indexOf('\\'); } String var = index == -1 ? target : target.substring(0, index); File file = resolveVariableExpression(var, false, depth + 1); if (file != null) { File original = file; for (int i = 0; i < count; i++) { if (file == null) { break; } file = file.getParentFile(); } if (file == null) { // Try again but with canonical files file = original.getCanonicalFile(); for (int i = 0; i < count; i++) { if (file == null) { break; } file = file.getParentFile(); } } } if (file != null && index != -1) { file = new File(file, target.substring(index + 1)); } return file; } } // See if it's an absolute path String filePath = path.replace('/', separatorChar); File resolved = new File(filePath); if (resolved.exists()) { return resolved; } // See if it's a relative path resolved = new File(myDir, filePath); if (resolved.exists()) { return resolved; } // Look up in shared path map (and record path for user editing in wizard // if not resolvable) // No -- this needs to be per project?? Only if it's used in multiple projects... resolved = myImporter.getPathMap().get(path); if (resolved != null) { return resolved; } if (record) { // Record the path expression such that the user can provide a resolution myImporter.getPathMap().put(path, null); } // Workspace path? if (path.startsWith("/")) { // It's / on Windows too // Workspace path resolved = myImporter.resolveWorkspacePath(this, path, record); if (resolved != null) { return resolved; } if (path.indexOf('/', 1) == -1 && path.indexOf('\\', 1) == -1) { String name = path.substring(1); // If we can't resolve workspace paths, try looking relative // to the current project; dependent projects are often there File parent = myDir.getParentFile(); if (parent != null) { File sibling = new File(parent, name); if (sibling.exists()) { return sibling; } } // Libraries are also often children File child = new File(myDir, name); if (child.exists()) { return child; } } } else if (path.startsWith("$%7B")) { // E.g. "<value>$%7BPARENT-2-PARENT_LOC%7D/Users</value>" // This corresponds to {PARENT_LOC}/../../ int start = 4; int end = path.indexOf("%7D", 4); if (end != -1) { String sub = path.substring(start, end); File expression = resolveVariableExpression(sub, false, depth + 1); if (expression != null) { String suffix = path.substring(end + 3); if (suffix.isEmpty()) { return expression; } else { resolved = new File(expression, suffix.replace('/', separatorChar)); if (resolved.exists()) { return resolved; } } } } } else { // Path variable? int index = path.indexOf('/'); if (index == -1) { index = path.indexOf('\\'); } String var; if (index == -1) { var = path; } else { var = path.substring(0, index); } Map<String, String> map = getLinkedResourceMap(); String expression = map.get(var); if (expression == null || expression.equals(var)) { map = getProjectVariableMap(); expression = map.get(var); } File file; if (expression != null) { if (expression.startsWith("file:")) { file = SdkUtils.urlToFile(expression); } else { file = resolveVariableExpression(expression, false, depth + 1); } } else { file = myImporter.resolvePathVariable(this, var, false); } if (file != null) { if (index == -1) { return file; } else { resolved = new File(file, path.substring(index + 1)); if (resolved.exists()) { return resolved; } } } } return null; } private void initAndroidProject() throws IOException { myAndroidProject = hasNature("com.android.ide.eclipse.adt.AndroidNature"); if (!myAndroidProject && getProjectDocument() == null) { myAndroidProject = GradleImport.isAdtProjectDir(myDir); } myNdkProject = myAndroidProject && (hasNature("org.eclipse.cdt.core.cnature") || hasNature("org.eclipse.cdt.core.ccnature") || new File(myDir, "jni" + separator + "Android.mk").exists()); } private boolean hasNature(String nature) throws IOException { Document document = getProjectDocument(); if (document != null) { NodeList natures = document.getElementsByTagName("nature"); for (int i = 0; i < natures.getLength(); i++) { Node element = natures.item(i); String value = getStringValue((Element)element); if (nature.equals(value)) { return true; } } } return false; } private void initLanguageLevel() throws IOException { if (myLanguageLevel == null) { myLanguageLevel = DEFAULT_LANGUAGE_LEVEL; // default File file = new File(myDir, ".settings" + separator + "org.eclipse.jdt.core.prefs"); if (file.exists()) { Properties properties = PropertiesUtil.getProperties(file); if (properties != null) { String source = properties.getProperty("org.eclipse.jdt.core.compiler.source"); if (source != null) { myLanguageLevel = source; } } } } } private void initProguard(@NonNull Properties properties) { myLocalProguardFiles = Lists.newArrayList(); mySdkProguardFiles = Lists.newArrayList(); String proguardConfig = properties.getProperty(PROGUARD_CONFIG); if (proguardConfig != null && !proguardConfig.isEmpty()) { // 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)) { mySdkProguardFiles.add(new File(path.substring(SDK_PROPERTY_REF.length()).replace('/', separatorChar))); } else if (path.startsWith(HOME_PROPERTY_REF)) { myImporter.getSummary().reportIgnoredUserHomeProGuardFile(path); } else { File proguardConfigFile = new File(path.replace('/', separatorChar)); if (!proguardConfigFile.isAbsolute()) { proguardConfigFile = new File(myDir, proguardConfigFile.getPath()); } if (proguardConfigFile.isFile()) { myLocalProguardFiles.add(proguardConfigFile); } } } } } @NonNull public File getDir() { return myDir; } @NonNull public File getCanonicalDir() { return myCanonicalDir; } public boolean isLibrary() { return myLibrary; } @NonNull public List<File> getLocalProguardFiles() { assert isAndroidProject(); return myLocalProguardFiles; } @NonNull public List<File> getSdkProguardFiles() { assert isAndroidProject(); return mySdkProguardFiles; } @NonNull public File getResourceDir() { assert isAndroidProject(); return new File(myDir, FD_RES); } @NonNull public File getAssetsDir() { assert isAndroidProject(); return new File(myDir, FD_ASSETS); } @NonNull private File getClassPathFile() { return new File(myDir, GradleImport.ECLIPSE_DOT_CLASSPATH); } @NonNull public Document getManifestDoc() throws IOException { assert isAndroidProject(); if (myManifestDoc == null) { File file = getManifestFile(); myManifestDoc = myImporter.getXmlDocument(file, true); } return myManifestDoc; } @NonNull File getManifestFile() { assert isAndroidProject(); return new File(myDir, ANDROID_MANIFEST_XML); } @Nullable public Properties getProjectProperties() throws IOException { if (myProjectProperties == null) { assert isAndroidProject(); File file = getProjectPropertiesFile(); if (file.exists()) { myProjectProperties = PropertiesUtil.getProperties(file); } else { myProjectProperties = new Properties(); } } return myProjectProperties; } private File getProjectPropertiesFile() { return new File(myDir, FN_PROJECT_PROPERTIES); } @Nullable private Document getProjectDocument() throws IOException { if (myProjectDoc == null) { File file = new File(myDir, GradleImport.ECLIPSE_DOT_PROJECT); if (file.exists()) { myProjectDoc = myImporter.getXmlDocument(file, false); } } return myProjectDoc; } public boolean isAndroidProject() { return myAndroidProject; } public boolean isNdkProject() { return myNdkProject; } @Nullable public File getInstrumentationDir() { return myInstrumentationDir; } @Nullable public String getPackage() { assert isAndroidProject(); return myPackage; } @NonNull public List<File> getSourcePaths() { return mySourcePaths; } @NonNull public List<String> getInferredLibraries() { return myInferredLibraries == null ? Collections.<String>emptyList() : myInferredLibraries; } @NonNull public List<File> getJarPaths() { return myJarPaths; } @NonNull public List<File> getTestJarPaths() { return myInstrumentationJarPaths != null ? myInstrumentationJarPaths : Collections.<File>emptyList(); } @NonNull public List<File> getNativeLibs() { return myNativeLibs != null ? myNativeLibs : Collections.<File>emptyList(); } @Nullable public File getNativeSources() { return myNativeSources; } @Nullable public String getNativeModuleName() { return myNativeModuleName; } @Nullable public File getOutputDir() { return myOutputDir; } /** * Returns "1.6", "1.7", etc */ @NonNull public String getLanguageLevel() { return myLanguageLevel; } @NonNull public String getName() { return myName != null ? myName : myDir.getName(); } @NonNull public AndroidVersion getMinSdkVersion() { assert isAndroidProject(); return myMinSdkVersion != null ? myMinSdkVersion : AndroidVersion.DEFAULT; } @NonNull public AndroidVersion getTargetSdkVersion() { assert isAndroidProject(); return myTargetSdkVersion != null ? myTargetSdkVersion : getMinSdkVersion(); } @NonNull public AndroidVersion getCompileSdkVersion() { assert isAndroidProject(); return myVersion == null ? new AndroidVersion(GradleImport.CURRENT_COMPILE_VERSION, null) : myVersion; } @NonNull public List<EclipseProject> getDirectLibraries() { return myDirectLibraries; } @NonNull public List<EclipseProject> getAllLibraries() { if (myAllLibraries == null) { if (myDirectLibraries.isEmpty()) { return myDirectLibraries; } List<EclipseProject> all = new ArrayList<EclipseProject>(); Set<EclipseProject> seen = Sets.newHashSet(); Set<EclipseProject> path = Sets.newHashSet(); seen.add(this); path.add(this); addLibraryProjects(all, seen, path); myAllLibraries = all; } return myAllLibraries; } private void addLibraryProjects(@NonNull Collection<EclipseProject> collection, @NonNull Set<EclipseProject> seen, @NonNull Set<EclipseProject> path) { for (EclipseProject library : myDirectLibraries) { if (seen.contains(library)) { if (path.contains(library)) { myImporter.reportWarning(library, library.getDir(), "Internal error: cyclic library dependency for " + library); } continue; } collection.add(library); seen.add(library); path.add(library); // Recurse library.addLibraryProjects(collection, seen, path); path.remove(library); } } @Override public int compareTo(@NonNull EclipseProject other) { return myDir.compareTo(other.myDir); } @Override public String toString() { return myDir.getPath(); } @Nullable public EclipseImportModule getModule() { return myModule; } public void setModule(@Nullable EclipseImportModule module) { myModule = module; } }