ProjectCreatorpublic class ProjectCreator extends Object Creates the basic files needed to get an Android project up and running. Also
allows creation of IntelliJ project files. |
Fields Summary |
---|
private static final String | PH_JAVA_FOLDERPackage path substitution string used in template files, i.e. "PACKAGE_PATH" | private static final String | PH_PACKAGEPackage name substitution string used in template files, i.e. "PACKAGE" | private static final String | PH_ACTIVITY_NAMEActivity name substitution string used in template files, i.e. "ACTIVITY_NAME". | private static final String | PH_PROJECT_NAMEProject name substitution string used in template files, i.e. "PROJECT_NAME". | private static final String | FOLDER_TESTS | public static final Pattern | RE_PROJECT_NAMEPattern for characters accepted in a project name. Since this will be used as a
directory name, we're being a bit conservative on purpose: dot and space cannot be used. | public static final String | CHARS_PROJECT_NAMEList of valid characters for a project name. Used for display purposes. | public static final Pattern | RE_PACKAGE_NAMEPattern for characters accepted in a package name. A package is list of Java identifier
separated by a dot. We need to have at least one dot (e.g. a two-level package name).
A Java identifier cannot start by a digit. | public static final String | CHARS_PACKAGE_NAMEList of valid characters for a project name. Used for display purposes. | public static final Pattern | RE_ACTIVITY_NAMEPattern for characters accepted in an activity name, which is a Java identifier. | public static final String | CHARS_ACTIVITY_NAMEList of valid characters for a project name. Used for display purposes. | private final OutputLevel | mLevel | private final com.android.sdklib.ISdkLog | mLog | private final String | mSdkFolder |
Methods Summary |
---|
private boolean | checkFileContainsRegexp(java.io.File file, java.lang.String regexp)Returns true if any line of the input file contains the requested regexp.
Pattern p = Pattern.compile(regexp);
try {
BufferedReader in = new BufferedReader(new FileReader(file));
String line;
while ((line = in.readLine()) != null) {
if (p.matcher(line).find()) {
return true;
}
}
in.close();
} catch (Exception e) {
// ignore
}
return false;
| private java.lang.String | combinePackageActivityNames(java.lang.String packageName, java.lang.String activityName)
// Activity Name can have 3 forms:
// - ".Name" means this is a class name in the given package name.
// The full FQCN is thus packageName + ".Name"
// - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
// - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
// To be valid, the package name should have at least two components. This is checked
// later during the creation of the build.xml file, so we just need to detect there's
// a dot but not at pos==0.
int pos = activityName.indexOf('.");
if (pos == 0) {
return packageName + activityName;
} else if (pos > 0) {
return activityName;
} else {
return packageName + "." + activityName;
}
| private java.io.File | createDirs(java.io.File parent, java.lang.String name)Creates a new folder, along with any parent folders that do not exists.
final File newFolder = new File(parent, name);
boolean existedBefore = true;
if (!newFolder.exists()) {
if (!newFolder.mkdirs()) {
throw new ProjectCreateException("Could not create directory: %1$s", newFolder);
}
existedBefore = false;
}
if (newFolder.isDirectory()) {
if (!newFolder.canWrite()) {
throw new ProjectCreateException("Path is not writable: %1$s", newFolder);
}
} else {
throw new ProjectCreateException("Path is not a directory: %1$s", newFolder);
}
if (!existedBefore) {
try {
println("Created directory %1$s", newFolder.getCanonicalPath());
} catch (IOException e) {
throw new ProjectCreateException(
"Could not determine canonical path of created directory", e);
}
}
return newFolder;
| public void | createProject(java.lang.String folderPath, java.lang.String projectName, java.lang.String packageName, java.lang.String activityName, com.android.sdklib.IAndroidTarget target, boolean isTestProject)Creates a new project.
The caller should have already checked and sanitized the parameters.
// create project folder if it does not exist
File projectFolder = new File(folderPath);
if (!projectFolder.exists()) {
boolean created = false;
Throwable t = null;
try {
created = projectFolder.mkdirs();
} catch (Exception e) {
t = e;
}
if (created) {
println("Created project directory: %1$s", projectFolder);
} else {
mLog.error(t, "Could not create directory: %1$s", projectFolder);
return;
}
} else {
Exception e = null;
String error = null;
try {
String[] content = projectFolder.list();
if (content == null) {
error = "Project folder '%1$s' is not a directory.";
} else if (content.length != 0) {
error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead.";
}
} catch (Exception e1) {
e = e1;
}
if (e != null || error != null) {
mLog.error(e, error, projectFolder, SdkConstants.androidCmdName());
}
}
try {
// first create the project properties.
// location of the SDK goes in localProperty
ProjectProperties localProperties = ProjectProperties.create(folderPath,
PropertyType.LOCAL);
localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
localProperties.save();
// target goes in default properties
ProjectProperties defaultProperties = ProjectProperties.create(folderPath,
PropertyType.DEFAULT);
defaultProperties.setAndroidTarget(target);
defaultProperties.save();
// create an empty build.properties
ProjectProperties buildProperties = ProjectProperties.create(folderPath,
PropertyType.BUILD);
buildProperties.save();
// create the map for place-holders of values to replace in the templates
final HashMap<String, String> keywords = new HashMap<String, String>();
// create the required folders.
// compute src folder path
final String packagePath =
stripString(packageName.replace(".", File.separator),
File.separatorChar);
// put this path in the place-holder map for project files that needs to list
// files manually.
keywords.put(PH_JAVA_FOLDER, packagePath);
keywords.put(PH_PACKAGE, packageName);
if (activityName != null) {
keywords.put(PH_ACTIVITY_NAME, activityName);
}
// Take the project name from the command line if there's one
if (projectName != null) {
keywords.put(PH_PROJECT_NAME, projectName);
} else {
if (activityName != null) {
// Use the activity as project name
keywords.put(PH_PROJECT_NAME, activityName);
} else {
// We need a project name. Just pick up the basename of the project
// directory.
projectName = projectFolder.getName();
keywords.put(PH_PROJECT_NAME, projectName);
}
}
// create the source folder and the java package folders.
String srcFolderPath = SdkConstants.FD_SOURCES + File.separator + packagePath;
File sourceFolder = createDirs(projectFolder, srcFolderPath);
String javaTemplate = "java_file.template";
String activityFileName = activityName + ".java";
if (isTestProject) {
javaTemplate = "java_tests_file.template";
activityFileName = activityName + "Test.java";
}
installTemplate(javaTemplate, new File(sourceFolder, activityFileName),
keywords, target);
// create the generate source folder
srcFolderPath = SdkConstants.FD_GEN_SOURCES + File.separator + packagePath;
sourceFolder = createDirs(projectFolder, srcFolderPath);
// create other useful folders
File resourceFodler = createDirs(projectFolder, SdkConstants.FD_RESOURCES);
createDirs(projectFolder, SdkConstants.FD_OUTPUT);
createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS);
if (isTestProject == false) {
/* Make res files only for non test projects */
File valueFolder = createDirs(resourceFodler, SdkConstants.FD_VALUES);
installTemplate("strings.template", new File(valueFolder, "strings.xml"),
keywords, target);
File layoutFolder = createDirs(resourceFodler, SdkConstants.FD_LAYOUT);
installTemplate("layout.template", new File(layoutFolder, "main.xml"),
keywords, target);
}
/* Make AndroidManifest.xml and build.xml files */
String manifestTemplate = "AndroidManifest.template";
if (isTestProject) {
manifestTemplate = "AndroidManifest.tests.template";
}
installTemplate(manifestTemplate,
new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML),
keywords, target);
installTemplate("build.template",
new File(projectFolder, SdkConstants.FN_BUILD_XML),
keywords);
// if this is not a test project, then we create one.
if (isTestProject == false) {
// create the test project folder.
createDirs(projectFolder, FOLDER_TESTS);
File testProjectFolder = new File(folderPath, FOLDER_TESTS);
createProject(testProjectFolder.getAbsolutePath(), projectName, packageName,
activityName, target, true /*isTestProject*/);
}
} catch (ProjectCreateException e) {
mLog.error(e, null);
} catch (IOException e) {
mLog.error(e, null);
}
| private boolean | extractPackageFromManifest(java.io.File manifestFile, java.util.Map outKeywords)Extracts a "full" package & activity name from an AndroidManifest.xml.
The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_NAME}.
When no activity is found, this key is not created.
try {
final String nsPrefix = "android";
final String nsURI = SdkConstants.NS_RESOURCES;
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
public String getNamespaceURI(String prefix) {
if (nsPrefix.equals(prefix)) {
return nsURI;
}
return XMLConstants.NULL_NS_URI;
}
public String getPrefix(String namespaceURI) {
if (nsURI.equals(namespaceURI)) {
return nsPrefix;
}
return null;
}
@SuppressWarnings("unchecked")
public Iterator getPrefixes(String namespaceURI) {
if (nsURI.equals(namespaceURI)) {
ArrayList<String> list = new ArrayList<String>();
list.add(nsPrefix);
return list.iterator();
}
return null;
}
});
InputSource source = new InputSource(new FileReader(manifestFile));
String packageName = xpath.evaluate("/manifest/@package", source);
source = new InputSource(new FileReader(manifestFile));
// Select the "android:name" attribute of all <activity> nodes but only if they
// contain a sub-node <intent-filter><action> with an "android:name" attribute which
// is 'android.intent.action.MAIN' and an <intent-filter><category> with an
// "android:name" attribute which is 'android.intent.category.LAUNCHER'
String expression = String.format("/manifest/application/activity" +
"[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
"intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
"/@%1$s:name", nsPrefix);
NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
XPathConstants.NODESET);
// If we get here, both XPath expressions were valid so we're most likely dealing
// with an actual AndroidManifest.xml file. The nodes may not have the requested
// attributes though, if which case we should warn.
if (packageName == null || packageName.length() == 0) {
mLog.error(null,
"Missing <manifest package=\"...\"> in '%1$s'",
manifestFile.getName());
return false;
}
// Get the first activity that matched earlier. If there is no activity,
// activityName is set to an empty string and the generated "combined" name
// will be in the form "package." (with a dot at the end).
String activityName = "";
if (activityNames.getLength() > 0) {
activityName = activityNames.item(0).getNodeValue();
}
if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
println("WARNING: There is more than one activity defined in '%1$s'.\n" +
"Only the first one will be used. If this is not appropriate, you need\n" +
"to specify one of these values manually instead:",
manifestFile.getName());
for (int i = 0; i < activityNames.getLength(); i++) {
String name = activityNames.item(i).getNodeValue();
name = combinePackageActivityNames(packageName, name);
println("- %1$s", name);
}
}
if (activityName.length() == 0) {
mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
"No activity will be generated.",
nsPrefix, manifestFile.getName());
} else {
outKeywords.put(PH_ACTIVITY_NAME, activityName);
}
outKeywords.put(PH_PACKAGE, packageName);
return true;
} catch (IOException e) {
mLog.error(e, "Failed to read %1$s", manifestFile.getName());
} catch (XPathExpressionException e) {
Throwable t = e.getCause();
mLog.error(t == null ? e : t,
"Failed to parse %1$s",
manifestFile.getName());
}
return false;
| private void | installFullPathTemplate(java.lang.String sourcePath, java.io.File destFile, java.util.Map placeholderMap)Installs a new file that is based on a template.
Each match of each key from the place-holder map in the template will be replaced with its
corresponding value in the created file.
boolean existed = destFile.exists();
try {
BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
BufferedReader in = new BufferedReader(new FileReader(sourcePath));
String line;
while ((line = in.readLine()) != null) {
for (String key : placeholderMap.keySet()) {
line = line.replace(key, placeholderMap.get(key));
}
out.write(line);
out.newLine();
}
out.close();
in.close();
} catch (Exception e) {
throw new ProjectCreateException(e, "Could not access %1$s: %2$s",
destFile, e.getMessage());
}
println("%1$s file %2$s",
existed ? "Updated" : "Added",
destFile);
| private void | installTemplate(java.lang.String templateName, java.io.File destFile, java.util.Map placeholderMap, com.android.sdklib.IAndroidTarget target)Installs a new file that is based on a template file provided by a given target.
Each match of each key from the place-holder map in the template will be replaced with its
corresponding value in the created file.
// query the target for its template directory
String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
final String sourcePath = templateFolder + File.separator + templateName;
installFullPathTemplate(sourcePath, destFile, placeholderMap);
| private void | installTemplate(java.lang.String templateName, java.io.File destFile, java.util.Map placeholderMap)Installs a new file that is based on a template file provided by the tools folder.
Each match of each key from the place-holder map in the template will be replaced with its
corresponding value in the created file.
// query the target for its template directory
String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
final String sourcePath = templateFolder + File.separator + templateName;
installFullPathTemplate(sourcePath, destFile, placeholderMap);
| private void | println(java.lang.String format, java.lang.Object args)Prints a message unless silence is enabled.
This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
{@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
if (mLevel != OutputLevel.SILENT) {
if (!format.endsWith("\n")) {
format += "\n";
}
mLog.printf(format, args);
}
| private static java.lang.String | stripString(java.lang.String s, char strip)Strips the string of beginning and trailing characters (multiple
characters will be stripped, example stripString("..test...", '.')
results in "test";
final int sLen = s.length();
int newStart = 0, newEnd = sLen - 1;
while (newStart < sLen && s.charAt(newStart) == strip) {
newStart++;
}
while (newEnd >= 0 && s.charAt(newEnd) == strip) {
newEnd--;
}
/*
* newEnd contains a char we want, and substring takes end as being
* exclusive
*/
newEnd++;
if (newStart >= sLen || newEnd < 0) {
return "";
}
return s.substring(newStart, newEnd);
| public void | updateProject(java.lang.String folderPath, com.android.sdklib.IAndroidTarget target, java.lang.String projectName)Updates an existing project.
Workflow:
- Check AndroidManifest.xml is present (required)
- Check there's a default.properties with a target *or* --target was specified
- Update default.prop if --target was specified
- Refresh/create "sdk" in local.properties
- Build.xml: create if not present or no ) in it
// project folder must exist and be a directory, since this is an update
File projectFolder = new File(folderPath);
if (!projectFolder.isDirectory()) {
mLog.error(null, "Project folder '%1$s' is not a valid directory, this is not an Android project you can update.",
projectFolder);
return;
}
// Check AndroidManifest.xml is present
File androidManifest = new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML);
if (!androidManifest.isFile()) {
mLog.error(null,
"%1$s not found in '%2$s', this is not an Android project you can update.",
SdkConstants.FN_ANDROID_MANIFEST_XML,
folderPath);
return;
}
// Check there's a default.properties with a target *or* --target was specified
ProjectProperties props = ProjectProperties.load(folderPath, PropertyType.DEFAULT);
if (props == null || props.getProperty(ProjectProperties.PROPERTY_TARGET) == null) {
if (target == null) {
mLog.error(null,
"There is no %1$s file in '%2$s'. Please provide a --target to the '%3$s update' command.",
PropertyType.DEFAULT.getFilename(),
folderPath,
SdkConstants.androidCmdName());
return;
}
}
// Update default.prop if --target was specified
if (target != null) {
// we already attempted to load the file earlier, if that failed, create it.
if (props == null) {
props = ProjectProperties.create(folderPath, PropertyType.DEFAULT);
}
// set or replace the target
props.setAndroidTarget(target);
try {
props.save();
println("Updated %1$s", PropertyType.DEFAULT.getFilename());
} catch (IOException e) {
mLog.error(e, "Failed to write %1$s file in '%2$s'",
PropertyType.DEFAULT.getFilename(),
folderPath);
return;
}
}
// Refresh/create "sdk" in local.properties
// because the file may already exists and contain other values (like apk config),
// we first try to load it.
props = ProjectProperties.load(folderPath, PropertyType.LOCAL);
if (props == null) {
props = ProjectProperties.create(folderPath, PropertyType.LOCAL);
}
// set or replace the sdk location.
props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
try {
props.save();
println("Updated %1$s", PropertyType.LOCAL.getFilename());
} catch (IOException e) {
mLog.error(e, "Failed to write %1$s file in '%2$s'",
PropertyType.LOCAL.getFilename(),
folderPath);
return;
}
// Build.xml: create if not present or no <androidinit/> in it
File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML);
boolean needsBuildXml = projectName != null || !buildXml.exists();
if (!needsBuildXml) {
// Note that "<androidinit" must be followed by either a whitespace, a "/" (for the
// XML /> closing tag) or an end-of-line. This way we know the XML tag is really this
// one and later we will be able to use an "androidinit2" tag or such as necessary.
needsBuildXml = !checkFileContainsRegexp(buildXml, "<androidinit(?:\\s|/|$)");
if (needsBuildXml) {
println("File %1$s is too old and needs to be updated.", SdkConstants.FN_BUILD_XML);
}
}
if (needsBuildXml) {
// create the map for place-holders of values to replace in the templates
final HashMap<String, String> keywords = new HashMap<String, String>();
// Take the project name from the command line if there's one
if (projectName != null) {
keywords.put(PH_PROJECT_NAME, projectName);
} else {
extractPackageFromManifest(androidManifest, keywords);
if (keywords.containsKey(PH_ACTIVITY_NAME)) {
// Use the activity as project name
keywords.put(PH_PROJECT_NAME, keywords.get(PH_ACTIVITY_NAME));
} else {
// We need a project name. Just pick up the basename of the project
// directory.
projectName = projectFolder.getName();
keywords.put(PH_PROJECT_NAME, projectName);
}
}
if (mLevel == OutputLevel.VERBOSE) {
println("Regenerating %1$s with project name %2$s",
SdkConstants.FN_BUILD_XML,
keywords.get(PH_PROJECT_NAME));
}
try {
installTemplate("build.template",
new File(projectFolder, SdkConstants.FN_BUILD_XML),
keywords);
} catch (ProjectCreateException e) {
mLog.error(e, null);
}
}
|
|