blob: 9c4d6b17b4e78de0fdcba97878ff045b7f807a09 [file] [log] [blame]
/*
* 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.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.ide.common.repository.SdkMavenRepository;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.SdkManager;
import com.android.sdklib.repository.local.LocalSdk;
import com.android.tools.idea.gradle.util.PropertiesUtil;
import com.android.utils.*;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.collect.*;
import com.google.common.io.Files;
import com.google.common.primitives.Bytes;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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 static com.android.SdkConstants.*;
import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_NDK;
import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK;
import static com.android.xml.AndroidManifest.NODE_INSTRUMENTATION;
import static com.google.common.base.Charsets.UTF_8;
import static java.io.File.separator;
import static java.io.File.separatorChar;
/**
* Importer which can generate Android Gradle projects.
* <p/>
* It currently only supports importing ADT projects, so it will require
* some tweaks to handle importing from other types of projects.
* <p/>
* The importer primarily imports complete ADT projects into complete (new) Gradle
* projects, but it can also be used to import one or more ADT projects into an
* existing Gradle project as new modules. See {@link #exportIntoProject} for
* details on how to do this.
* <p/>
* TODO:
* <ul>
* <li>Migrate SDK folder from local.properties. If should make doubly sure that
* the repository you point to contains the app support library and other
* libraries that may be needed.</li>
* <li>Consider whether I can make this import mechanism work for Maven and plain
* sources as well?</li>
* <li>Make it optional whether we replace the directory structure with the Gradle one?</li>
* <li>Allow migrating a project in-place?</li>
* <li>If I have a workspace, check to see if there are problem markers and if
* so warn that the project may not be buildable</li>
* <li>Optional: at the end of the import, migrate Eclipse settings too --
* such as code styles, compiler flags (especially those for the
* project), ask about enabling eclipse key bindings, etc?</li>
* <li>If replaceJars=false, insert *comments* in the source code for potential
* replacements such that users don't forget and consider switching in the future</li>
* <li>Figure out if we can reuse fragments from the default freemarker templates for
* the code generation part.</li>
* <li>Allow option to preserve module nesting hierarchy. It currently flattens.</li>
* <li>Make it possible to use this wizard to migrate an already exported Eclipse project?</li>
* <li>Consider making the export create an HTML file and open in browser?</li>
* </ul>
*/
public class GradleImport {
public static final String NL = SdkUtils.getLineSeparator();
public static final int CURRENT_COMPILE_VERSION = 21;
public static final String CURRENT_BUILD_TOOLS_VERSION = SdkConstants.MIN_BUILD_TOOLS_VERSION;
public static final String ANDROID_GRADLE_PLUGIN = GRADLE_PLUGIN_NAME + GRADLE_PLUGIN_RECOMMENDED_VERSION;
public static final String MAVEN_URL_PROPERTY = "android.mavenRepoUrl";
public static final String ECLIPSE_DOT_CLASSPATH = ".classpath";
public static final String ECLIPSE_DOT_PROJECT = ".project";
public static final String IMPORT_SUMMARY_TXT = "import-summary.txt";
public static final String MAVEN_REPOSITORY;
static {
String repository = System.getProperty(MAVEN_URL_PROPERTY);
if (repository == null) {
repository = System.getenv("MAVEN_URL"); // as used by the CI server, and also all other test projects
}
if (repository == null) {
repository = "jcenter()";
}
else {
repository = "jcenter();" + NL + " " +
"maven { url '" + repository + "' }";
}
MAVEN_REPOSITORY = repository;
}
/**
* Whether we should place the repository definitions in the global build.gradle rather
* than in each module
*/
static final boolean DECLARE_GLOBAL_REPOSITORIES = true;
private static final String WORKSPACE_PROPERTY = "android.eclipseWorkspace";
private final List<String> myWarnings = Lists.newArrayList();
private final List<String> myErrors = Lists.newArrayList();
private List<? extends ImportModule> myRootModules;
private Set<ImportModule> myModules;
private ImportSummary mySummary;
private File myWorkspaceLocation;
private File myGradleWrapperLocation;
private File mySdkLocation;
private File myNdkLocation;
private SdkManager mySdkManager;
private Set<String> myHandledJars = Sets.newHashSet();
private Map<String, File> myWorkspaceProjects;
/**
* Whether we should convert project names to lowercase module names
*/
private boolean myGradleNameStyle = true;
/**
* Whether we should try to replace jars with dependencies
*/
private boolean myReplaceJars = true;
/**
* Whether we should try to replace libs with dependencies
*/
private boolean myReplaceLibs = true;
/**
* Whether the importer is in "import into existing project" mode. In that case,
* some different choices are made; for example, we don't rewrite the module name
* to "app" when importing a single project; we preserve the project name.
*/
private boolean myImportIntoExisting;
/**
* Whether we should emit per-module repository definitions
*/
@SuppressWarnings("PointlessBooleanExpression")
private boolean myPerModuleRepositories = !DECLARE_GLOBAL_REPOSITORIES;
private Map<String, File> myPathMap = Maps.newTreeMap();
/**
* Map of modules user chose to import with their new names. Can be
* <code>null</code> when all modules will be imported
*/
private Map<File, String> mySelectedModules;
private boolean myDefaultEncodingInitialized;
private Charset myDefaultEncoding;
private Map<File, EclipseProject> myProjectMap = Maps.newHashMap();
public GradleImport() {
String workspace = System.getProperty(WORKSPACE_PROPERTY);
if (workspace != null) {
myWorkspaceLocation = new File(workspace);
}
}
public static boolean isEclipseProjectDir(@Nullable File file) {
return file != null &&
file.isDirectory() &&
new File(file, ECLIPSE_DOT_CLASSPATH).exists() &&
new File(file, ECLIPSE_DOT_PROJECT).exists();
}
public static boolean isAdtProjectDir(@Nullable File file) {
return new File(file, ANDROID_MANIFEST_XML).exists() &&
(isEclipseProjectDir(file) ||
(new File(file, FD_RES).exists() && ((new File(file, FD_SOURCES).exists() || new File(file, "jni").exists()))));
}
@Nullable
private static File getDirFromLocalProperties(@NonNull File projectDir, @NonNull String property) {
File localProperties = new File(projectDir, FN_LOCAL_PROPERTIES);
if (localProperties.exists()) {
try {
Properties properties = PropertiesUtil.getProperties(localProperties);
if (properties != null) {
String sdk = properties.getProperty(property);
if (sdk != null) {
File dir = new File(sdk);
if (dir.exists()) {
return dir;
}
else {
dir = new File(sdk.replace('/', separatorChar));
if (dir.exists()) {
return dir;
}
}
}
}
}
catch (IOException e) {
// ignore properties
}
}
return null;
}
public static boolean isEclipseWorkspaceDir(@NonNull File file) {
return file.isDirectory() && new File(file, ".metadata" + separator + "version.ini").exists();
}
private static String generateProguardFileList(List<File> localRules, List<File> sdkRules) {
assert !localRules.isEmpty() || !sdkRules.isEmpty();
StringBuilder sb = new StringBuilder();
for (File rule : sdkRules) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("getDefaultProguardFile('");
sb.append(escapeGroovyStringLiteral(rule.getName()));
sb.append("')");
}
for (File rule : localRules) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("'");
// Note: project config files are flattened into the module structure (see
// ImportModule#copyInto handler)
sb.append(escapeGroovyStringLiteral(rule.getName()));
sb.append("'");
}
return sb.toString();
}
private static void appendDependencies(@NonNull StringBuilder sb, @NonNull ImportModule module) throws IOException {
if (!module.getDirectDependencies().isEmpty() ||
!module.getDependencies().isEmpty() ||
!module.getJarDependencies().isEmpty() ||
!module.getTestDependencies().isEmpty() ||
!module.getTestJarDependencies().isEmpty()) {
sb.append(NL);
sb.append("dependencies {").append(NL);
for (ImportModule lib : module.getDirectDependencies()) {
if (lib.isReplacedWithDependency()) {
continue;
}
sb.append(" compile project('").append(lib.getModuleReference()).append("')").append(NL);
}
for (GradleCoordinate dependency : module.getDependencies()) {
sb.append(" compile '").append(dependency.toString()).append("'").append(NL);
}
for (File jar : module.getJarDependencies()) {
String path = jar.getPath().replace(separatorChar, '/'); // Always / in gradle
sb.append(" compile files('").append(escapeGroovyStringLiteral(path)).append("')").append(NL);
}
for (GradleCoordinate dependency : module.getTestDependencies()) {
sb.append(" androidTestCompile '").append(dependency.toString()).append("'").append(NL);
}
for (File jar : module.getTestJarDependencies()) {
String path = jar.getPath().replace(separatorChar, '/');
sb.append(" androidTestCompile files('").append(escapeGroovyStringLiteral(path)).append("')").append(NL);
}
sb.append("}").append(NL);
}
}
@NotNull
public static String escapeGroovyStringLiteral(@NotNull String s) {
StringBuilder sb = new StringBuilder(s.length() + 5);
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (c == '\\' || c == '\'') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
private static String formatMessage(@Nullable String project, @Nullable File file, @NonNull String message) {
StringBuilder sb = new StringBuilder();
if (project != null) {
sb.append("Project ").append(project).append(":");
}
if (file != null) {
sb.append(file.getPath());
sb.append(":\n");
}
sb.append(message);
return sb.toString();
}
/**
* Returns true if the given file should be ignored (note: this may not return
* true for files inside ignored folders, so to determine if a given file should
* really be ignored you should check all ancestors as well, or only call this as
* part of a recursive directory traversal)
*/
static boolean isIgnoredFile(File file) {
String name = file.getName();
return name.equals(".svn") ||
name.equals(".git") ||
name.equals(".hg") ||
name.equals(".DS_Store") ||
name.endsWith("~") && name.length() > 1;
}
/**
* Copies the file with the given source encoding to a UTF-8 text file
*/
private static void copyTextFileWithEncoding(@NonNull File source, @NonNull File dest, @NonNull Charset sourceEncoding)
throws IOException {
if (!Charsets.UTF_8.equals(sourceEncoding)) {
String text = Files.toString(source, sourceEncoding);
Files.write(text, dest, Charsets.UTF_8);
}
else {
// Already using the right encoding
Files.copy(source, dest);
}
}
/**
* Returns true if this file is believed to be a source file (so it should be encoded
* as a UTF-8 file in the imported project
*
* @param file the file to check
* @return true if it is known to be a source file
*/
@VisibleForTesting
public static boolean isTextFile(@NonNull File file) {
String name = file.getName();
return name.endsWith(DOT_JAVA) ||
name.endsWith(DOT_XML) ||
name.endsWith(DOT_AIDL) ||
name.endsWith(DOT_FS) ||
name.endsWith(DOT_RS) ||
name.endsWith(DOT_RSH) ||
name.endsWith(DOT_RSH) ||
name.endsWith(DOT_TXT) ||
name.endsWith(DOT_GRADLE) ||
name.endsWith(DOT_PROPERTIES) ||
name.endsWith(".cfg") ||
name.endsWith(".pro") ||
name.endsWith(".h") ||
name.endsWith(".c") ||
name.endsWith(".cpp");
}
/**
* Computes the relative path for the given file inside another directory
*/
@Nullable
public static File computeRelativePath(@NonNull File canonicalBase, @NonNull File file) throws IOException {
File canonical = file.getCanonicalFile();
String canonicalPath = canonical.getPath();
if (canonicalPath.startsWith(canonicalBase.getPath())) {
int length = canonicalBase.getPath().length();
if (canonicalPath.length() == length) {
return new File(".");
}
else if (canonicalPath.charAt(length) == separatorChar) {
return new File(canonicalPath.substring(length + 1));
}
else {
return new File(canonicalPath.substring(length));
}
}
return null;
}
/**
* Imports the given projects. Note that this just reads in the project state;
* it does not actually write out a Gradle project. For that, you should call
* {@link #exportProject(java.io.File, boolean)}.
*
* @param projectDirs the project directories to import
* @throws IOException if something is wrong
*/
public void importProjects(@NonNull List<File> projectDirs) throws IOException {
mySummary = new ImportSummary(this);
myProjectMap.clear();
myHandledJars.clear();
myWarnings.clear();
myErrors.clear();
myWorkspaceProjects = null;
myRootModules = Collections.emptyList();
myModules = Sets.newHashSet();
for (File file : projectDirs) {
if (file.isFile()) {
assert !file.isDirectory();
file = file.getParentFile();
}
guessWorkspace(file);
if (isAdtProjectDir(file)) {
guessSdk(file);
guessNdk(file);
try {
EclipseProject.getProject(this, file);
}
catch (ImportException e) {
// Already recorded
return;
}
catch (Exception e) {
reportError(null, file, e.toString(), false);
return;
}
}
else {
reportError(null, file, "Not a recognized project: " + file, false);
return;
}
}
// Find unique projects. (We can register projects under multiple paths
// if the dir and the canonical dir differ, so pick unique values here)
Set<EclipseProject> projects = Sets.newHashSet(myProjectMap.values());
myRootModules = EclipseProject.performImport(this, projects);
for (ImportModule module : myRootModules) {
myModules.add(module);
myModules.addAll(module.getAllDependencies());
}
}
/**
* Sets location of gradle wrapper to copy into exported project, if known
*/
@NonNull
public GradleImport setGradleWrapperLocation(@NonNull File gradleWrapper) {
myGradleWrapperLocation = gradleWrapper;
return this;
}
/**
* Returns the location of the SDK to use with the import, if known
*/
@Nullable
public File getSdkLocation() {
return mySdkLocation;
}
/**
* Sets location of the SDK to use with the import, if known
*/
@NonNull
public GradleImport setSdkLocation(@Nullable File sdkLocation) {
mySdkLocation = sdkLocation;
return this;
}
@Nullable
public SdkManager getSdkManager() {
if (mySdkManager == null && mySdkLocation != null && mySdkLocation.exists()) {
ILogger logger = new StdLogger(StdLogger.Level.INFO);
mySdkManager = SdkManager.createManager(mySdkLocation.getPath(), logger);
}
return mySdkManager;
}
/**
* Sets SDK manager to use with the import, if known
*/
@NonNull
public GradleImport setSdkManager(@NonNull SdkManager sdkManager) {
mySdkManager = sdkManager;
mySdkLocation = new File(sdkManager.getLocation());
return this;
}
/**
* Gets location of the SDK to use with the import, if known
*/
@Nullable
public File getNdkLocation() {
return myNdkLocation;
}
/**
* Sets location of the SDK to use with the import, if known
*/
@NonNull
public GradleImport setNdkLocation(@Nullable File ndkLocation) {
myNdkLocation = ndkLocation;
return this;
}
/**
* Gets location of Eclipse workspace, if known
*/
@Nullable
public File getEclipseWorkspace() {
return myWorkspaceLocation;
}
/**
* Sets location of Eclipse workspace, if known
*/
public GradleImport setEclipseWorkspace(@NonNull File workspace) {
myWorkspaceLocation = workspace;
assert myWorkspaceLocation.exists() : workspace.getPath();
myWorkspaceProjects = null;
return this;
}
/**
* Whether import should attempt to replace jars with dependencies
*/
public boolean isReplaceJars() {
return myReplaceJars;
}
/**
* Whether import should attempt to replace jars with dependencies
*/
@NonNull
public GradleImport setReplaceJars(boolean replaceJars) {
myReplaceJars = replaceJars;
return this;
}
/**
* Whether import should attempt to replace inlined library projects with dependencies
*/
public boolean isReplaceLibs() {
return myReplaceLibs;
}
/**
* Whether import should attempt to replace inlined library projects with dependencies
*/
public GradleImport setReplaceLibs(boolean replaceLibs) {
myReplaceLibs = replaceLibs;
return this;
}
/**
* Whether we're in "import into existing project" mode, or import complete new project
* mode (the default)
*/
public boolean isImportIntoExisting() {
return myImportIntoExisting;
}
/**
* Sets whether we're in "import into existing project" mode, or import complete new project
* mode (the default). When importing into existing projects some different behaviors are
* applied; for example, we don't change the module name to "app" when there is just one
* project being imported.
*/
public void setImportIntoExisting(boolean importIntoExisting) {
myImportIntoExisting = importIntoExisting;
}
/**
* Returns whether the importer emits the repository definitions in each module's build.gradle
* rather than at the top level in the shared build.gradle
*/
public boolean isPerModuleRepositories() {
return myPerModuleRepositories;
}
/**
* Sets whether the importer emits the repository definitions in each module's build.gradle
* rather than at the top level in the shared build.gradle
*/
public void setPerModuleRepositories(boolean perModuleRepositories) {
myPerModuleRepositories = perModuleRepositories;
}
/**
* Whether import should lower-case module names from ADT project names
*/
public boolean isGradleNameStyle() {
return myGradleNameStyle;
}
/**
* Whether import should lower-case module names from ADT project names
*/
@NonNull
public GradleImport setGradleNameStyle(boolean lowerCase) {
myGradleNameStyle = lowerCase;
return this;
}
private void guessWorkspace(@NonNull File projectDir) {
if (myWorkspaceLocation == null) {
File dir = projectDir.getParentFile();
while (dir != null) {
if (isEclipseWorkspaceDir(dir)) {
setEclipseWorkspace(dir);
break;
}
dir = dir.getParentFile();
}
}
}
private void guessSdk(@NonNull File projectDir) {
if (mySdkLocation == null) {
mySdkLocation = getDirFromLocalProperties(projectDir, PROPERTY_SDK);
if (mySdkLocation == null && myWorkspaceLocation != null) {
mySdkLocation = getDirFromWorkspaceSetting(getAdtSettingsFile(), "com.android.ide.eclipse.adt.sdk");
}
}
}
private void guessNdk(@NonNull File projectDir) {
if (myNdkLocation == null) {
myNdkLocation = getDirFromLocalProperties(projectDir, PROPERTY_NDK);
if (myNdkLocation == null && myWorkspaceLocation != null) {
myNdkLocation = getDirFromWorkspaceSetting(getNdkSettingsFile(), "ndkLocation");
}
}
}
@Nullable
private Charset getEncodingFromWorkspaceSetting() {
if (myWorkspaceLocation != null && !myDefaultEncodingInitialized) {
myDefaultEncodingInitialized = true;
File settings = getEncodingSettingsFile();
if (settings.exists()) {
try {
Properties properties = PropertiesUtil.getProperties(settings);
if (properties != null) {
String encodingName = properties.getProperty("encoding");
if (encodingName != null) {
try {
myDefaultEncoding = Charset.forName(encodingName);
}
catch (UnsupportedCharsetException uce) {
reportWarning((ImportModule)null, settings, "Unknown charset " + encodingName);
}
}
}
}
catch (IOException e) {
// ignore properties
}
}
}
return myDefaultEncoding;
}
@Nullable
private File getDirFromWorkspaceSetting(@NonNull File settings, @NonNull String property) {
//noinspection VariableNotUsedInsideIf
if (myWorkspaceLocation != null) {
if (settings.exists()) {
try {
Properties properties = PropertiesUtil.getProperties(settings);
if (properties != null) {
String path = properties.getProperty(property);
if (path == null) {
return null;
}
File dir = new File(path);
if (dir.exists()) {
return dir;
}
else {
dir = new File(path.replace('/', separatorChar));
if (dir.exists()) {
return dir;
}
}
}
}
catch (IOException e) {
// Ignore workspace data
}
}
}
return null;
}
@Nullable
public File resolveWorkspacePath(@Nullable EclipseProject fromProject, @NonNull String path, boolean record) {
if (path.isEmpty()) {
return null;
}
// If file within project, must match on all prefixes
for (Map.Entry<String, File> entry : myPathMap.entrySet()) {
String workspacePath = entry.getKey();
File file = entry.getValue();
if (file != null && path.startsWith(workspacePath)) {
if (path.equals(workspacePath)) {
return file;
}
else {
path = path.substring(workspacePath.length());
if (path.charAt(0) == '/' || path.charAt(0) == separatorChar) {
path = path.substring(1);
}
File resolved = new File(file, path.replace('/', separatorChar));
if (resolved.exists()) {
return resolved;
}
}
}
}
if (fromProject != null && myWorkspaceLocation == null) {
guessWorkspace(fromProject.getDir());
}
if (myWorkspaceLocation != null) {
// Is the file present directly in the workspace?
char first = path.charAt(0);
if (first != '/') {
return null;
}
File f = new File(myWorkspaceLocation, path.substring(1).replace('/', separatorChar));
if (f.exists()) {
myPathMap.put(path, f);
return f;
}
// Other files may be in other file systems, mapped by a .location link in the
// workspace metadata
if (myWorkspaceProjects == null) {
myWorkspaceProjects = Maps.newHashMap();
File projectDir = new File(myWorkspaceLocation, ".metadata" +
separator +
".plugins" +
separator +
"org.eclipse.core.resources" +
separator +
".projects");
File[] projects = projectDir.exists() ? projectDir.listFiles() : null;
byte[] target = "URI//file:".getBytes(Charsets.US_ASCII);
if (projects != null) {
for (File project : projects) {
File location = new File(project, ".location");
if (location.exists()) {
try {
byte[] bytes = Files.toByteArray(location);
int start = Bytes.indexOf(bytes, target);
if (start != -1) {
int end = start + target.length;
for (; end < bytes.length; end++) {
if (bytes[end] == (byte)0) {
break;
}
}
try {
int length = end - start;
String s = new String(bytes, start, length, UTF_8);
s = s.substring(5); // skip URI//
File file = SdkUtils.urlToFile(s);
if (file.exists()) {
String name = project.getName();
myWorkspaceProjects.put('/' + name, file);
//noinspection ConstantConditions
}
}
catch (Throwable t) {
// Ignore binary data we can't read
}
}
}
catch (IOException e) {
reportWarning((ImportModule)null, location, "Can't read .location file");
}
}
}
}
}
// Is it just a project root?
File project = myWorkspaceProjects.get(path);
if (project != null) {
myPathMap.put(path, project);
return project;
}
// If file within project, must match on all prefixes
for (Map.Entry<String, File> entry : myWorkspaceProjects.entrySet()) {
String workspacePath = entry.getKey();
File file = entry.getValue();
if (file != null && path.startsWith(workspacePath)) {
if (path.equals(workspacePath)) {
return file;
}
else {
path = path.substring(workspacePath.length());
if (path.charAt(0) == '/' || path.charAt(0) == separatorChar) {
path = path.substring(1);
}
File resolved = new File(file, path.replace('/', separatorChar));
if (resolved.exists()) {
return resolved;
}
}
}
}
// Record path as one we need to resolve
if (record) {
myPathMap.put(path, null);
}
}
else if (record) {
// Record path as one we need to resolve
myPathMap.put(path, null);
}
return null;
}
public void exportProject(@NonNull File destDir, boolean allowNonEmpty) throws IOException {
mySummary.setDestDir(destDir);
if (!isImportIntoExisting()) {
createDestDir(destDir, allowNonEmpty);
createProjectBuildGradle(new File(destDir, FN_BUILD_GRADLE));
exportGradleWrapper(destDir);
exportLocalProperties(destDir);
}
exportSettingsGradle(new File(destDir, FN_SETTINGS_GRADLE), isImportIntoExisting());
for (ImportModule module : getModulesToImport()) {
exportModule(new File(destDir, module.getModuleName()), module);
}
mySummary.write(new File(destDir, IMPORT_SUMMARY_TXT));
}
private Iterable<? extends ImportModule> getModulesToImport() {
if (mySelectedModules == null) {
return myRootModules;
}
else {
ImmutableSet.Builder<ImportModule> builder = ImmutableSet.builder();
for (ImportModule module : myRootModules) {
File dir = module.getDir();
if (mySelectedModules.containsKey(dir)) {
String name = mySelectedModules.get(dir);
if (name != null) {
module.setModuleName(name);
}
builder.add(module);
}
}
return builder.build();
}
}
@Deprecated
public void setModulesToImport(Map<String, File> modules) {
mySelectedModules = Maps.newHashMap();
for (File module : modules.values()) {
mySelectedModules.put(module, null);
}
}
/**
* Like {@link #exportProject(java.io.File, boolean)}, but writes into an existing
* project instead of creating a new one.
* <p>
* <b>NOTE</b>: When performing an import into an existing project, note that
* you should call {@link #setImportIntoExisting(boolean)} before the call to
* read in projects ({@link #importProjects(java.util.List)}. Note also that
* you should call {@link #setPerModuleRepositories(boolean)} with a suitable
* value based on whether the existing project defines shared repositories.
* This is similar to how we pass the "perModuleRepositories" variable to
* our Freemarker templates (such as
* templates/gradle-projects/NewAndroidModule/root/build.gradle.ftl ) so it
* can decide whether to include this info in the new module. In Studio we
* set it based on whether $PROJECT/build.gradle contains "repositories" (this
* is done in NewModuleWizard).
* </p>
*
* @param projectDir the root directory containing the project to write into
* @param updateSettings whether the importer should attempt to update the settings.gradle
* file in the project or not. Clients such as Android Studio may
* wish to pass false here in order to handle this part
* @param writeSummary whether we should generate an import summary
* @param destDirMap optional map from ADT project dir to destination directory to
* write each module as.
* @return the list of imported module directories
*/
@NonNull
public List<File> exportIntoProject(@NonNull File projectDir,
boolean updateSettings,
boolean writeSummary,
@Nullable Map<File, File> destDirMap) throws IOException {
mySummary.setDestDir(projectDir);
List<File> imported = Lists.newArrayListWithExpectedSize(myRootModules.size());
for (ImportModule module : getModulesToImport()) {
File moduleDir = null;
if (destDirMap != null) {
moduleDir = destDirMap.get(module.getDir());
}
if (moduleDir == null) {
moduleDir = new File(projectDir, module.getModuleName());
if (moduleDir.exists()) {
module.pickUniqueName(projectDir);
moduleDir = new File(projectDir, module.getModuleName());
assert !moduleDir.exists();
}
}
exportModule(moduleDir, module);
imported.add(moduleDir);
}
if (updateSettings) {
exportSettingsGradle(new File(projectDir, FN_SETTINGS_GRADLE), true);
}
if (writeSummary) {
mySummary.write(new File(projectDir, IMPORT_SUMMARY_TXT));
}
return imported;
}
private void exportGradleWrapper(@NonNull File destDir) throws IOException {
if (myGradleWrapperLocation != null && myGradleWrapperLocation.exists()) {
File gradlewDest = new File(destDir, FN_GRADLE_WRAPPER_UNIX);
copyDir(new File(myGradleWrapperLocation, FN_GRADLE_WRAPPER_UNIX), gradlewDest, null, false, null);
boolean madeExecutable = gradlewDest.setExecutable(true);
if (!madeExecutable) {
reportWarning((ImportModule)null, gradlewDest, "Could not make gradle wrapper script executable");
}
copyDir(new File(myGradleWrapperLocation, FN_GRADLE_WRAPPER_WIN), new File(destDir, FN_GRADLE_WRAPPER_WIN), null, false, null);
copyDir(new File(myGradleWrapperLocation, FD_GRADLE), new File(destDir, FD_GRADLE), null, false, null);
}
}
// Write local.properties file
private void exportLocalProperties(@NonNull File destDir) throws IOException {
boolean needsNdk = needsNdk();
if (myNdkLocation != null && needsNdk || mySdkLocation != null) {
Properties properties = new Properties();
if (mySdkLocation != null) {
properties.setProperty(PROPERTY_SDK, mySdkLocation.getPath());
}
if (myNdkLocation != null && needsNdk) {
properties.setProperty(PROPERTY_NDK, myNdkLocation.getPath());
}
File path = new File(destDir, FN_LOCAL_PROPERTIES);
String comments = "# This file must *NOT* be checked into Version Control Systems,\n" +
"# as it contains information specific to your local configuration.\n" +
"\n" +
"# Location of the SDK. This is only used by Gradle.\n";
PropertiesUtil.savePropertiesToFile(properties, path, comments);
}
}
/**
* Returns true if this project appears to need the NDK
*/
public boolean needsNdk() {
for (ImportModule module : myModules) {
if (module.isNdkProject()) {
return true;
}
}
return false;
}
private void exportModule(File destDir, ImportModule module) throws IOException {
mkdirs(destDir);
createModuleBuildGradle(new File(destDir, FN_BUILD_GRADLE), module);
module.copyInto(destDir);
}
@SuppressWarnings("MethodMayBeStatic")
/** Ensure that the given directory exists, and if it can't be created, report an I/O error */
public void mkdirs(@NonNull File destDir) throws IOException {
if (!destDir.exists()) {
boolean ok = destDir.mkdirs();
if (!ok) {
reportError(null, destDir, "Could not make directory " + destDir);
}
}
}
private void createModuleBuildGradle(@NonNull File file, ImportModule module) throws IOException {
StringBuilder sb = new StringBuilder(500);
if (module.isApp() || module.isAndroidLibrary()) {
//noinspection PointlessBooleanExpression,ConstantConditions
if (myPerModuleRepositories) {
appendRepositories(sb, true);
}
if (module.isApp()) {
sb.append("apply plugin: 'com.android.application'").append(NL);
}
else {
assert module.isAndroidLibrary();
sb.append("apply plugin: 'com.android.library'").append(NL);
}
sb.append(NL);
//noinspection PointlessBooleanExpression,ConstantConditions
if (myPerModuleRepositories) {
sb.append("repositories {").append(NL);
sb.append(" ").append(MAVEN_REPOSITORY).append(NL);
sb.append("}").append(NL);
sb.append(NL);
}
sb.append("android {").append(NL);
AndroidVersion compileSdkVersion = module.getCompileSdkVersion();
AndroidVersion minSdkVersion = module.getMinSdkVersion();
AndroidVersion targetSdkVersion = module.getTargetSdkVersion();
String compileSdkVersionString = compileSdkVersion.isPreview()
? '\'' + AndroidTargetHash.getPlatformHashString(compileSdkVersion) + '\''
: Integer.toString(compileSdkVersion.getApiLevel());
String addOn = module.getAddOn();
if (addOn != null) {
compileSdkVersionString = '\'' + addOn + '\'';
}
String minSdkVersionString = minSdkVersion.isPreview()
? '\'' + module.getMinSdkVersion().getCodename() + '\''
: Integer.toString(module.getMinSdkVersion().getApiLevel());
String targetSdkVersionString = targetSdkVersion.isPreview()
? '\'' + module.getTargetSdkVersion().getCodename() + '\''
: Integer.toString(module.getTargetSdkVersion().getApiLevel());
sb.append(" compileSdkVersion ").append(compileSdkVersionString).append(NL);
sb.append(" buildToolsVersion \"").append(getBuildToolsVersion()).append("\"").append(NL);
sb.append(NL);
sb.append(" defaultConfig {").append(NL);
if (module.getPackage() != null && module.isApp()) {
sb.append(" applicationId \"").append(module.getPackage()).append('"').append(NL);
}
if (minSdkVersion.getApiLevel() > 1) {
sb.append(" minSdkVersion ").append(minSdkVersionString).append(NL);
}
if (targetSdkVersion.getApiLevel() > 1 && compileSdkVersion.getApiLevel() > 3) {
sb.append(" targetSdkVersion ").append(targetSdkVersionString).append(NL);
}
String languageLevel = module.getLanguageLevel();
if (!languageLevel.equals(EclipseProject.DEFAULT_LANGUAGE_LEVEL)) {
sb.append(" compileOptions {").append(NL);
String level = languageLevel.replace('.', '_'); // 1.6 => 1_6
sb.append(" sourceCompatibility JavaVersion.VERSION_").append(level).append(NL);
sb.append(" targetCompatibility JavaVersion.VERSION_").append(level).append(NL);
sb.append(" }").append(NL);
}
if (module.isNdkProject() && module.getNativeModuleName() != null) {
sb.append(NL);
sb.append(" ndk {").append(NL);
sb.append(" moduleName \"").append(module.getNativeModuleName()).append("\"").append(NL);
sb.append(" }").append(NL);
}
if (module.getInstrumentationDir() != null) {
sb.append(NL);
File manifestFile = new File(module.getInstrumentationDir(), ANDROID_MANIFEST_XML);
assert manifestFile.exists() : manifestFile;
Document manifest = getXmlDocument(manifestFile, true);
if (manifest != null && manifest.getDocumentElement() != null) {
String pkg = manifest.getDocumentElement().getAttribute(ATTR_PACKAGE);
if (pkg != null && !pkg.isEmpty()) {
sb.append(" testApplicationId \"").append(pkg).append("\"").append(NL);
}
NodeList list = manifest.getElementsByTagName(NODE_INSTRUMENTATION);
if (list.getLength() > 0) {
Element tag = (Element)list.item(0);
String runner = tag.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (runner != null && !runner.isEmpty()) {
sb.append(" testInstrumentationRunner \"").append(runner).append("\"").append(NL);
}
Attr attr = tag.getAttributeNodeNS(ANDROID_URI, "functionalTest");
if (attr != null) {
sb.append(" testFunctionalTest ").append(attr.getValue()).append(NL);
}
attr = tag.getAttributeNodeNS(ANDROID_URI, "handleProfiling");
if (attr != null) {
sb.append(" testHandlingProfiling ").append(attr.getValue()).append(NL);
}
}
}
}
sb.append(" }").append(NL);
sb.append(NL);
List<File> localRules = module.getLocalProguardFiles();
List<File> sdkRules = module.getSdkProguardFiles();
if (!localRules.isEmpty() || !sdkRules.isEmpty()) {
// User specified ProGuard rules; replicate exactly
sb.append(" buildTypes {").append(NL);
sb.append(" release {").append(NL);
sb.append(" minifyEnabled true").append(NL);
sb.append(" proguardFiles ");
sb.append(generateProguardFileList(localRules, sdkRules)).append(NL);
sb.append(" }").append(NL);
sb.append(" }").append(NL);
}
else {
// User didn't specify ProGuard rules; put in defaults (but off)
sb.append(" buildTypes {").append(NL);
sb.append(" release {").append(NL);
sb.append(" minifyEnabled false").append(NL);
sb.append(" proguardFiles getDefaultProguardFile('proguard-" + "android.txt'), 'proguard-rules.txt'").append(NL);
sb.append(" }").append(NL);
sb.append(" }").append(NL);
}
sb.append("}").append(NL);
appendDependencies(sb, module);
}
else if (module.isJavaLibrary()) {
//noinspection PointlessBooleanExpression,ConstantConditions
if (myPerModuleRepositories) {
appendRepositories(sb, false);
}
sb.append("apply plugin: 'java'").append(NL);
String languageLevel = module.getLanguageLevel();
if (!languageLevel.equals(EclipseProject.DEFAULT_LANGUAGE_LEVEL)) {
sb.append(NL);
sb.append("sourceCompatibility = \"");
sb.append(languageLevel);
sb.append("\"").append(NL);
sb.append("targetCompatibility = \"");
sb.append(languageLevel);
sb.append("\"").append(NL);
}
appendDependencies(sb, module);
}
else {
assert false : module;
}
Files.write(sb.toString(), file, UTF_8);
}
String getBuildToolsVersion() {
SdkManager sdkManager = getSdkManager();
if (sdkManager != null) {
final BuildToolInfo buildTool = sdkManager.getLatestBuildTool();
if (buildTool != null) {
return buildTool.getRevision().toString();
}
}
return CURRENT_BUILD_TOOLS_VERSION;
}
private void appendRepositories(@NonNull StringBuilder sb, boolean needAndroidPlugin) {
//noinspection PointlessBooleanExpression,ConstantConditions
if (myPerModuleRepositories) {
//noinspection SpellCheckingInspection
sb.append("buildscript {").append(NL);
sb.append(" repositories {").append(NL);
sb.append(" ").append(MAVEN_REPOSITORY).append(NL);
sb.append(" }").append(NL);
if (needAndroidPlugin) {
sb.append(" dependencies {").append(NL);
sb.append(" classpath '" + ANDROID_GRADLE_PLUGIN + "'").append(NL);
sb.append(" }").append(NL);
}
sb.append("}").append(NL);
}
}
private void createProjectBuildGradle(@NonNull File file) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("// Top-level build file where you can add configuration options common to all sub-projects/modules.");
//noinspection PointlessBooleanExpression,ConstantConditions
if (!myPerModuleRepositories) {
sb.append(NL);
//noinspection SpellCheckingInspection
sb.append("buildscript {").append(NL);
sb.append(" repositories {").append(NL);
sb.append(" ").append(MAVEN_REPOSITORY).append(NL);
sb.append(" }").append(NL);
sb.append(" dependencies {").append(NL);
sb.append(" classpath '" + ANDROID_GRADLE_PLUGIN + "'").append(NL);
sb.append(" }").append(NL);
sb.append("}").append(NL);
sb.append(NL);
//noinspection SpellCheckingInspection
sb.append("allprojects {").append(NL);
sb.append(" repositories {").append(NL);
sb.append(" ").append(MAVEN_REPOSITORY).append(NL);
sb.append(" }").append(NL);
sb.append("}");
}
sb.append(NL);
Files.write(sb.toString(), file, UTF_8);
}
private void exportSettingsGradle(@NonNull File file, boolean append) throws IOException {
StringBuilder sb = new StringBuilder();
if (append) {
if (!file.exists()) {
append = false;
}
else {
// Ensure that the new include statements are separate code statements, not
// for example inserted at the end of a // line comment
String existing = Files.toString(file, UTF_8);
if (!existing.endsWith(NL)) {
sb.append(NL);
}
}
}
for (ImportModule module : getModulesToImport()) {
sb.append("include '");
sb.append(module.getModuleReference());
sb.append("'");
sb.append(NL);
}
String code = sb.toString();
if (append) {
Files.append(code, file, UTF_8);
}
else {
Files.write(code, file, UTF_8);
}
}
private void createDestDir(@NonNull File destDir, boolean allowNonEmpty) throws IOException {
if (destDir.exists()) {
if (!allowNonEmpty) {
File[] files = destDir.listFiles();
if (files != null && files.length > 0) {
throw new IOException("Destination directory " + destDir + " should be empty");
}
}
}
else {
mkdirs(destDir);
}
}
@NonNull
public List<String> getWarnings() {
return myWarnings;
}
@NonNull
public List<String> getErrors() {
return myErrors;
}
/**
* Returns module names to module source locations mappings.
*/
@NonNull
public Map<String, File> getDetectedModuleLocations() {
TreeMap<String, File> modules = new TreeMap<String, File>();
for (ImportModule module : myModules) {
modules.put(module.getModuleName(), module.getCanonicalModuleDir());
}
return modules;
}
public void setImportModuleNames(Map<File, String> moduleLocationToName) {
mySelectedModules = ImmutableMap.copyOf(moduleLocationToName);
}
@NotNull
public Set<String> getProjectDependencies(String projectName) {
ImportModule module = null;
for (ImportModule m : myModules) {
if (Objects.equal(m.getModuleName(), projectName)) {
module = m;
break;
}
}
if (module == null) {
return ImmutableSet.of();
}
else {
ImmutableSet.Builder<String> deps = ImmutableSet.builder();
for (ImportModule importModule : module.getAllDependencies()) {
deps.add(importModule.getModuleName());
}
return deps.build();
}
}
@SuppressWarnings("MethodMayBeStatic")
public void reportError(@Nullable EclipseProject project, @Nullable File file, @NonNull String message) {
reportError(project, file, message, true);
}
public void reportError(@Nullable EclipseProject project, @Nullable File file, @NonNull String message, boolean abort) {
String text = formatMessage(project != null ? project.getName() : null, file, message);
myErrors.add(text);
if (abort) {
throw new ImportException(text);
}
}
public void reportWarning(@Nullable ImportModule module, @Nullable File file, @NonNull String message) {
String moduleName = module != null ? module.getOriginalName() : null;
myWarnings.add(formatMessage(moduleName, file, message));
}
public void reportWarning(@Nullable EclipseProject project, @Nullable File file, @NonNull String message) {
String moduleName = project != null ? project.getName() : null;
myWarnings.add(formatMessage(moduleName, file, message));
}
@Nullable
File resolvePathVariable(@Nullable EclipseProject fromProject, @NonNull String name, boolean record) throws IOException {
File file = myPathMap.get(name);
if (file != null) {
return file;
}
if (fromProject != null && myWorkspaceLocation == null) {
guessWorkspace(fromProject.getDir());
}
String value = null;
Properties properties = getJdtSettingsProperties(false);
if (properties != null) {
value = properties.getProperty("org.eclipse.jdt.core.classpathVariable." + name);
}
if (value == null) {
properties = getPathSettingsProperties(false);
if (properties != null) {
value = properties.getProperty("pathvariable." + name);
}
}
if (value == null) {
if (record) {
myPathMap.put(name, null);
}
return null;
}
file = new File(value.replace('/', separatorChar));
return file;
}
@Nullable
private Properties getJdtSettingsProperties(boolean mustExist) throws IOException {
File settings = getJdtSettingsFile();
if (!settings.exists()) {
if (mustExist) {
reportError(null, settings, "Settings file does not exist");
}
return null;
}
return PropertiesUtil.getProperties(settings);
}
private File getRuntimeSettingsDir() {
return new File(getWorkspaceLocation(), ".metadata" + separator +
".plugins" + separator +
"org.eclipse.core.runtime" + separator +
".settings");
}
private File getJdtSettingsFile() {
return new File(getRuntimeSettingsDir(), "org.eclipse.jdt.core.prefs");
}
private File getPathSettingsFile() {
return new File(getRuntimeSettingsDir(), "org.eclipse.core.resources.prefs");
}
private File getEncodingSettingsFile() {
return getPathSettingsFile();
}
private File getNdkSettingsFile() {
return new File(getRuntimeSettingsDir(), "com.android.ide.eclipse.ndk.prefs");
}
private File getAdtSettingsFile() {
return new File(getRuntimeSettingsDir(), "com.android.ide.eclipse.adt.prefs");
}
@Nullable
private Properties getPathSettingsProperties(boolean mustExist) throws IOException {
File settings = getPathSettingsFile();
if (!settings.exists()) {
if (mustExist) {
reportError(null, settings, "Settings file does not exist");
}
return null;
}
return PropertiesUtil.getProperties(settings);
}
private File getWorkspaceLocation() {
return myWorkspaceLocation;
}
@Nullable
Document getXmlDocument(File file, boolean namespaceAware) throws IOException {
String xml = Files.toString(file, UTF_8);
try {
return XmlUtils.parseDocument(xml, namespaceAware);
}
catch (Exception e) {
reportError(null, file, "Invalid XML file: " + file.getPath() + ":\n" + e.getMessage());
return null;
}
}
Map<File, EclipseProject> getProjectMap() {
return myProjectMap;
}
public ImportSummary getSummary() {
return mySummary;
}
void registerProject(@NonNull EclipseProject project) {
// Register not just this directory but the canonical versions too, since library
// references in project.properties can be relative and can be made canonical;
// we want to make sure that a project known by any of these versions of the paths
// are treated as the same
myProjectMap.put(project.getDir(), project);
myProjectMap.put(project.getDir().getAbsoluteFile(), project);
myProjectMap.put(project.getCanonicalDir(), project);
}
int getModuleCount() {
int moduleCount = 0;
for (ImportModule module : myModules) {
if (!module.isReplacedWithDependency()) {
moduleCount++;
}
}
return moduleCount;
}
/**
* Returns a path map for workspace paths
*/
public Map<String, File> getPathMap() {
return myPathMap;
}
/**
* Handles copying the given source into the given destination, whether the source
* is a file or directory. An optional handler can be used to perform special handling,
* such as skipping files or changing the destination.
*
* @param source the source file/directory to copy
* @param dest the destination for that file
* @param handler an optional copy handler
* @param updateEncoding if false, do not try to rewrite encodings to UTF-8
* @param sourceModule if non null, a corresponding module this source file belongs to
* (used to look up the default encoding if applicable)
*/
public void copyDir(@NonNull File source,
@NonNull File dest,
@Nullable CopyHandler handler,
boolean updateEncoding,
@Nullable ImportModule sourceModule) throws IOException {
if (handler != null && handler.handle(source, dest, updateEncoding, sourceModule)) {
return;
}
if (source.isDirectory()) {
if (isIgnoredFile(source)) {
// Skip version control files when generating the migrated project;
// it will only have fragments of the project, and in some cases moved
// around, so don't pick up partial VCS state
return;
}
mkdirs(dest);
File[] files = source.listFiles();
if (files != null) {
for (File child : files) {
copyDir(child, new File(dest, child.getName()), handler, updateEncoding, sourceModule);
}
}
// Delete empty directories. This happens for example when a whole source subdirectory
// turns out to only contain special files that are moved elsewhere (such as .aidl or resource files)
File[] copied = dest.listFiles();
if (copied != null && copied.length == 0) {
//noinspection ResultOfMethodCallIgnored
dest.delete();
}
}
else if (updateEncoding && isTextFile(source)
// Property files have their own special encoding; don't touch these
&& !source.getPath().endsWith(DOT_PROPERTIES)) {
copyTextFile(sourceModule, source, dest);
}
else {
Files.copy(source, dest);
}
}
public void copyTextFile(@Nullable ImportModule module, @NonNull File source, @NonNull File dest) throws IOException {
assert isTextFile(source) : source;
Charset encoding = null;
// A specific encoding configured for a given file always wins:
if (module != null) {
encoding = module.getFileEncoding(source);
if (encoding != null) {
copyTextFileWithEncoding(source, dest, encoding);
return;
}
encoding = module.getProjectEncoding(source);
}
if (encoding == null) {
encoding = getEncodingFromWorkspaceSetting();
}
// For XML files we can sometimes read the encoding right out of the XML prologue
if (SdkUtils.endsWithIgnoreCase(source.getPath(), DOT_XML)) {
String defaultCharset = encoding != null ? encoding.name() : SdkConstants.UTF_8;
String xml = PositionXmlParser.getXmlString(Files.toByteArray(source), defaultCharset);
// Replace prologue if it specifies the encoding
if (xml.startsWith("<?xml")) {
int prologueEnd = xml.indexOf("?>");
if (prologueEnd != -1) {
xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + xml.substring(prologueEnd + 2);
}
}
Files.write(xml, dest, Charsets.UTF_8);
}
else if (encoding != null) {
copyTextFileWithEncoding(source, dest, encoding);
}
else {
Files.copy(source, dest);
}
}
void markJarHandled(@NonNull File file) {
myHandledJars.add(file.getName());
}
boolean isJarHandled(@NonNull File file) {
return myHandledJars.contains(file.getName());
}
private boolean haveLocalRepository(@NonNull SdkMavenRepository repository) {
SdkManager sdkManager = getSdkManager();
if (sdkManager != null) {
LocalSdk localSdk = sdkManager.getLocalSdk();
if (repository.isInstalled(localSdk)) {
return true;
}
}
return repository.isInstalled(mySdkLocation);
}
public boolean needSupportRepository() {
return haveArtifact("com.android.support");
}
public boolean needGoogleRepository() {
return haveArtifact("com.google.android.gms");
}
private boolean haveArtifact(String groupId) {
for (ImportModule module : getModulesToImport()) {
for (GradleCoordinate dependency : module.getDependencies()) {
if (groupId.equals(dependency.getGroupId())) {
return true;
}
}
}
return false;
}
public boolean isMissingSupportRepository() {
return !haveLocalRepository(SdkMavenRepository.ANDROID);
}
public boolean isMissingGoogleRepository() {
return !haveLocalRepository(SdkMavenRepository.GOOGLE);
}
/**
* Interface used by the {@link #copyDir} handler
*/
public interface CopyHandler {
/**
* Optionally handle the given file; returns true if the file has been
* handled
*
* @param source the source file/directory to copy
* @param dest the destination for that file
* @param updateEncoding if false, do not try to rewrite encodings to UTF-8
* @param sourceModule if non null, a corresponding module this source file belongs to
*/
boolean handle(@NonNull File source, @NonNull File dest, boolean updateEncoding, @Nullable ImportModule sourceModule)
throws IOException;
}
private static class ImportException extends RuntimeException {
private String mMessage;
private ImportException(@NonNull String message) {
mMessage = message;
}
@Override
public String getMessage() {
return mMessage;
}
@Override
public String toString() {
return getMessage();
}
}
}