• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.sdklib.internal.project;
18 
19 import com.android.sdklib.IAndroidTarget;
20 import com.android.sdklib.ISdkLog;
21 import com.android.sdklib.SdkConstants;
22 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
23 import com.android.sdklib.xml.AndroidManifest;
24 import com.android.sdklib.xml.AndroidXPathFactory;
25 
26 import org.w3c.dom.NodeList;
27 import org.xml.sax.InputSource;
28 
29 import java.io.BufferedReader;
30 import java.io.BufferedWriter;
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.FileReader;
36 import java.io.FileWriter;
37 import java.io.IOException;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.regex.Pattern;
41 
42 import javax.xml.xpath.XPath;
43 import javax.xml.xpath.XPathConstants;
44 import javax.xml.xpath.XPathExpressionException;
45 
46 /**
47  * Creates the basic files needed to get an Android project up and running.
48  *
49  * @hide
50  */
51 public class ProjectCreator {
52 
53     /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */
54     private final static String PH_JAVA_FOLDER = "PACKAGE_PATH";
55     /** Package name substitution string used in template files, i.e. "PACKAGE" */
56     private final static String PH_PACKAGE = "PACKAGE";
57     /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME".
58      * @deprecated This is only used for older templates. For new ones see
59      * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */
60     private final static String PH_ACTIVITY_NAME = "ACTIVITY_NAME";
61     /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/
62     private final static String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME";
63     /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/
64     private final static String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME";
65     /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/
66     private final static String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME";
67     /** Original Activity class name substitution string used in class templates, i.e.
68      * "ACTIVITY_TESTED_CLASS_NAME".*/
69     private final static String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME";
70     /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */
71     private final static String PH_PROJECT_NAME = "PROJECT_NAME";
72     /** Application icon substitution string used in the manifest template */
73     private final static String PH_ICON = "ICON";
74 
75     /** Pattern for characters accepted in a project name. Since this will be used as a
76      * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */
77     public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+");
78     /** List of valid characters for a project name. Used for display purposes. */
79     public final static String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _";
80 
81     /** Pattern for characters accepted in a package name. A package is list of Java identifier
82      * separated by a dot. We need to have at least one dot (e.g. a two-level package name).
83      * A Java identifier cannot start by a digit. */
84     public static final Pattern RE_PACKAGE_NAME =
85         Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+");
86     /** List of valid characters for a project name. Used for display purposes. */
87     public final static String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _";
88 
89     /** Pattern for characters accepted in an activity name, which is a Java identifier. */
90     public static final Pattern RE_ACTIVITY_NAME =
91         Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
92     /** List of valid characters for a project name. Used for display purposes. */
93     public final static String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _";
94 
95 
96     public enum OutputLevel {
97         /** Silent mode. Project creation will only display errors. */
98         SILENT,
99         /** Normal mode. Project creation will display what's being done, display
100          * error but not warnings. */
101         NORMAL,
102         /** Verbose mode. Project creation will display what's being done, errors and warnings. */
103         VERBOSE;
104     }
105 
106     /**
107      * Exception thrown when a project creation fails, typically because a template
108      * file cannot be written.
109      */
110     private static class ProjectCreateException extends Exception {
111         /** default UID. This will not be serialized anyway. */
112         private static final long serialVersionUID = 1L;
113 
114         @SuppressWarnings("unused")
ProjectCreateException(String message)115         ProjectCreateException(String message) {
116             super(message);
117         }
118 
ProjectCreateException(Throwable t, String format, Object... args)119         ProjectCreateException(Throwable t, String format, Object... args) {
120             super(format != null ? String.format(format, args) : format, t);
121         }
122 
ProjectCreateException(String format, Object... args)123         ProjectCreateException(String format, Object... args) {
124             super(String.format(format, args));
125         }
126     }
127 
128     private final OutputLevel mLevel;
129 
130     private final ISdkLog mLog;
131     private final String mSdkFolder;
132 
ProjectCreator(String sdkFolder, OutputLevel level, ISdkLog log)133     public ProjectCreator(String sdkFolder, OutputLevel level, ISdkLog log) {
134         mSdkFolder = sdkFolder;
135         mLevel = level;
136         mLog = log;
137     }
138 
139     /**
140      * Creates a new project.
141      * <p/>
142      * The caller should have already checked and sanitized the parameters.
143      *
144      * @param folderPath the folder of the project to create.
145      * @param projectName the name of the project. The name must match the
146      *          {@link #RE_PROJECT_NAME} regex.
147      * @param packageName the package of the project. The name must match the
148      *          {@link #RE_PACKAGE_NAME} regex.
149      * @param activityEntry the activity of the project as it will appear in the manifest. Can be
150      *          null if no activity should be created. The name must match the
151      *          {@link #RE_ACTIVITY_NAME} regex.
152      * @param target the project target.
153      * @param pathToMainProject if non-null the project will be setup to test a main project
154      * located at the given path.
155      */
createProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, String pathToMainProject)156     public void createProject(String folderPath, String projectName,
157             String packageName, String activityEntry, IAndroidTarget target,
158             String pathToMainProject) {
159 
160         // create project folder if it does not exist
161         File projectFolder = new File(folderPath);
162         if (!projectFolder.exists()) {
163 
164             boolean created = false;
165             Throwable t = null;
166             try {
167                 created = projectFolder.mkdirs();
168             } catch (Exception e) {
169                 t = e;
170             }
171 
172             if (created) {
173                 println("Created project directory: %1$s", projectFolder);
174             } else {
175                 mLog.error(t, "Could not create directory: %1$s", projectFolder);
176                 return;
177             }
178         } else {
179             Exception e = null;
180             String error = null;
181             try {
182                 String[] content = projectFolder.list();
183                 if (content == null) {
184                     error = "Project folder '%1$s' is not a directory.";
185                 } else if (content.length != 0) {
186                     error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead.";
187                 }
188             } catch (Exception e1) {
189                 e = e1;
190             }
191 
192             if (e != null || error != null) {
193                 mLog.error(e, error, projectFolder, SdkConstants.androidCmdName());
194             }
195         }
196 
197         try {
198             boolean isTestProject = pathToMainProject != null;
199 
200             // first create the project properties.
201 
202             // location of the SDK goes in localProperty
203             ProjectProperties localProperties = ProjectProperties.create(folderPath,
204                     PropertyType.LOCAL);
205             localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
206             localProperties.save();
207 
208             // target goes in default properties
209             ProjectProperties defaultProperties = ProjectProperties.create(folderPath,
210                     PropertyType.DEFAULT);
211             defaultProperties.setAndroidTarget(target);
212             defaultProperties.save();
213 
214             // create a build.properties file with just the application package
215             ProjectProperties buildProperties = ProjectProperties.create(folderPath,
216                     PropertyType.BUILD);
217 
218             // only put application.package for older target where the rules file didn't.
219             // grab it through xpath
220             if (target.getVersion().getApiLevel() < 4) {
221                 buildProperties.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE, packageName);
222             }
223 
224             if (isTestProject) {
225                 buildProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT,
226                         pathToMainProject);
227             }
228 
229             buildProperties.save();
230 
231             // create the map for place-holders of values to replace in the templates
232             final HashMap<String, String> keywords = new HashMap<String, String>();
233 
234             // create the required folders.
235             // compute src folder path
236             final String packagePath =
237                 stripString(packageName.replace(".", File.separator),
238                         File.separatorChar);
239 
240             // put this path in the place-holder map for project files that needs to list
241             // files manually.
242             keywords.put(PH_JAVA_FOLDER, packagePath);
243             keywords.put(PH_PACKAGE, packageName);
244 
245 
246             // compute some activity related information
247             String fqActivityName = null, activityPath = null, activityClassName = null;
248             String originalActivityEntry = activityEntry;
249             String originalActivityClassName = null;
250             if (activityEntry != null) {
251                 if (isTestProject) {
252                     // append Test so that it doesn't collide with the main project activity.
253                     activityEntry += "Test";
254 
255                     // get the classname from the original activity entry.
256                     int pos = originalActivityEntry.lastIndexOf('.');
257                     if (pos != -1) {
258                         originalActivityClassName = originalActivityEntry.substring(pos + 1);
259                     } else {
260                         originalActivityClassName = originalActivityEntry;
261                     }
262                 }
263 
264                 // get the fully qualified name of the activity
265                 fqActivityName = AndroidManifest.combinePackageAndClassName(packageName,
266                         activityEntry);
267 
268                 // get the activity path (replace the . to /)
269                 activityPath = stripString(fqActivityName.replace(".", File.separator),
270                         File.separatorChar);
271 
272                 // remove the last segment, so that we only have the path to the activity, but
273                 // not the activity filename itself.
274                 activityPath = activityPath.substring(0,
275                         activityPath.lastIndexOf(File.separatorChar));
276 
277                 // finally, get the class name for the activity
278                 activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1);
279             }
280 
281             // at this point we have the following for the activity:
282             // activityEntry: this is the manifest entry. For instance .MyActivity
283             // fqActivityName: full-qualified class name: com.foo.MyActivity
284             // activityClassName: only the classname: MyActivity
285             // originalActivityClassName: the classname of the activity being tested (if applicable)
286 
287             // Add whatever activity info is needed in the place-holder map.
288             // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests).
289             if (target.getVersion().getApiLevel() < 4) { // legacy
290                 if (originalActivityEntry != null) {
291                     keywords.put(PH_ACTIVITY_NAME, originalActivityEntry);
292                 }
293             } else {
294                 // newer templates make a difference between the manifest entries, classnames,
295                 // as well as the main and test classes.
296                 if (activityEntry != null) {
297                     keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry);
298                     keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName);
299                     keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName);
300                     if (originalActivityClassName != null) {
301                         keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName);
302                     }
303                 }
304             }
305 
306             // Take the project name from the command line if there's one
307             if (projectName != null) {
308                 keywords.put(PH_PROJECT_NAME, projectName);
309             } else {
310                 if (activityClassName != null) {
311                     // Use the activity class name as project name
312                     keywords.put(PH_PROJECT_NAME, activityClassName);
313                 } else {
314                     // We need a project name. Just pick up the basename of the project
315                     // directory.
316                     projectName = projectFolder.getName();
317                     keywords.put(PH_PROJECT_NAME, projectName);
318                 }
319             }
320 
321             // create the source folder for the activity
322             if (activityClassName != null) {
323                 String srcActivityFolderPath = SdkConstants.FD_SOURCES + File.separator + activityPath;
324                 File sourceFolder = createDirs(projectFolder, srcActivityFolderPath);
325 
326                 String javaTemplate = isTestProject ? "java_tests_file.template"
327                         : "java_file.template";
328                 String activityFileName = activityClassName + ".java";
329 
330                 installTemplate(javaTemplate, new File(sourceFolder, activityFileName),
331                         keywords, target);
332             } else {
333                 // we should at least create 'src'
334                 createDirs(projectFolder, SdkConstants.FD_SOURCES);
335             }
336 
337             // create other useful folders
338             File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES);
339             createDirs(projectFolder, SdkConstants.FD_OUTPUT);
340             createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS);
341 
342             if (isTestProject == false) {
343                 /* Make res files only for non test projects */
344                 File valueFolder = createDirs(resourceFolder, SdkConstants.FD_VALUES);
345                 installTemplate("strings.template", new File(valueFolder, "strings.xml"),
346                         keywords, target);
347 
348                 File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_LAYOUT);
349                 installTemplate("layout.template", new File(layoutFolder, "main.xml"),
350                         keywords, target);
351 
352                 // create the icons
353                 if (installIcons(resourceFolder, target)) {
354                     keywords.put(PH_ICON, "android:icon=\"@drawable/icon\"");
355                 } else {
356                     keywords.put(PH_ICON, "");
357                 }
358             }
359 
360             /* Make AndroidManifest.xml and build.xml files */
361             String manifestTemplate = "AndroidManifest.template";
362             if (isTestProject) {
363                 manifestTemplate = "AndroidManifest.tests.template";
364             }
365 
366             installTemplate(manifestTemplate,
367                     new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML),
368                     keywords, target);
369 
370             installTemplate("build.template",
371                     new File(projectFolder, SdkConstants.FN_BUILD_XML),
372                     keywords);
373         } catch (ProjectCreateException e) {
374             mLog.error(e, null);
375         } catch (IOException e) {
376             mLog.error(e, null);
377         }
378     }
379 
380     /**
381      * Updates an existing project.
382      * <p/>
383      * Workflow:
384      * <ul>
385      * <li> Check AndroidManifest.xml is present (required)
386      * <li> Check there's a default.properties with a target *or* --target was specified
387      * <li> Update default.prop if --target was specified
388      * <li> Refresh/create "sdk" in local.properties
389      * <li> Build.xml: create if not present or no <androidinit(\w|/>) in it
390      * </ul>
391      *
392      * @param folderPath the folder of the project to update. This folder must exist.
393      * @param target the project target. Can be null.
394      * @param projectName The project name from --name. Can be null.
395      */
updateProject(String folderPath, IAndroidTarget target, String projectName)396     public void updateProject(String folderPath, IAndroidTarget target, String projectName) {
397         // since this is an update, check the folder does point to a project
398         File androidManifest = checkProjectFolder(folderPath);
399         if (androidManifest == null) {
400             return;
401         }
402 
403         // get the parent File.
404         File projectFolder = androidManifest.getParentFile();
405 
406         // Check there's a default.properties with a target *or* --target was specified
407         ProjectProperties props = ProjectProperties.load(folderPath, PropertyType.DEFAULT);
408         if (props == null || props.getProperty(ProjectProperties.PROPERTY_TARGET) == null) {
409             if (target == null) {
410                 mLog.error(null,
411                     "There is no %1$s file in '%2$s'. Please provide a --target to the '%3$s update' command.",
412                     PropertyType.DEFAULT.getFilename(),
413                     folderPath,
414                     SdkConstants.androidCmdName());
415                 return;
416             }
417         }
418 
419         // Update default.prop if --target was specified
420         if (target != null) {
421             // we already attempted to load the file earlier, if that failed, create it.
422             if (props == null) {
423                 props = ProjectProperties.create(folderPath, PropertyType.DEFAULT);
424             }
425 
426             // set or replace the target
427             props.setAndroidTarget(target);
428             try {
429                 props.save();
430                 println("Updated %1$s", PropertyType.DEFAULT.getFilename());
431             } catch (IOException e) {
432                 mLog.error(e, "Failed to write %1$s file in '%2$s'",
433                         PropertyType.DEFAULT.getFilename(),
434                         folderPath);
435                 return;
436             }
437         }
438 
439         // Refresh/create "sdk" in local.properties
440         // because the file may already exists and contain other values (like apk config),
441         // we first try to load it.
442         props = ProjectProperties.load(folderPath, PropertyType.LOCAL);
443         if (props == null) {
444             props = ProjectProperties.create(folderPath, PropertyType.LOCAL);
445         }
446 
447         // set or replace the sdk location.
448         props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
449         try {
450             props.save();
451             println("Updated %1$s", PropertyType.LOCAL.getFilename());
452         } catch (IOException e) {
453             mLog.error(e, "Failed to write %1$s file in '%2$s'",
454                     PropertyType.LOCAL.getFilename(),
455                     folderPath);
456             return;
457         }
458 
459         // Build.xml: create if not present or no <androidinit/> in it
460         File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML);
461         boolean needsBuildXml = projectName != null || !buildXml.exists();
462         if (!needsBuildXml) {
463             // Look for for a classname="com.android.ant.SetupTask" attribute
464             needsBuildXml = !checkFileContainsRegexp(buildXml,
465                     "classname=\"com.android.ant.SetupTask\"");  //$NON-NLS-1$
466         }
467         if (!needsBuildXml) {
468             // Note that "<setup" must be followed by either a whitespace, a "/" (for the
469             // XML /> closing tag) or an end-of-line. This way we know the XML tag is really this
470             // one and later we will be able to use an "androidinit2" tag or such as necessary.
471             needsBuildXml = !checkFileContainsRegexp(buildXml, "<setup(?:\\s|/|$)");  //$NON-NLS-1$
472         }
473         if (needsBuildXml) {
474             println("File %1$s is too old and needs to be updated.", SdkConstants.FN_BUILD_XML);
475         }
476 
477         if (needsBuildXml) {
478             // create the map for place-holders of values to replace in the templates
479             final HashMap<String, String> keywords = new HashMap<String, String>();
480 
481             // Take the project name from the command line if there's one
482             if (projectName != null) {
483                 keywords.put(PH_PROJECT_NAME, projectName);
484             } else {
485                 extractPackageFromManifest(androidManifest, keywords);
486                 if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) {
487                     String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME);
488                     // keep only the last segment if applicable
489                     int pos = activity.lastIndexOf('.');
490                     if (pos != -1) {
491                         activity = activity.substring(pos + 1);
492                     }
493 
494                     // Use the activity as project name
495                     keywords.put(PH_PROJECT_NAME, activity);
496                 } else {
497                     // We need a project name. Just pick up the basename of the project
498                     // directory.
499                     projectName = projectFolder.getName();
500                     keywords.put(PH_PROJECT_NAME, projectName);
501                 }
502             }
503 
504             if (mLevel == OutputLevel.VERBOSE) {
505                 println("Regenerating %1$s with project name %2$s",
506                         SdkConstants.FN_BUILD_XML,
507                         keywords.get(PH_PROJECT_NAME));
508             }
509 
510             try {
511                 installTemplate("build.template",
512                         new File(projectFolder, SdkConstants.FN_BUILD_XML),
513                         keywords);
514             } catch (ProjectCreateException e) {
515                 mLog.error(e, null);
516             }
517         }
518     }
519 
520     /**
521      * Updates a test project with a new path to the main (tested) project.
522      * @param folderPath the path of the test project.
523      * @param pathToMainProject the path to the main project, relative to the test project.
524      */
updateTestProject(final String folderPath, final String pathToMainProject)525     public void updateTestProject(final String folderPath, final String pathToMainProject) {
526         // since this is an update, check the folder does point to a project
527         if (checkProjectFolder(folderPath) == null) {
528             return;
529         }
530 
531         // check the path to the main project is valid.
532         File mainProject = new File(pathToMainProject);
533         String resolvedPath;
534         if (mainProject.isAbsolute() == false) {
535             mainProject = new File(folderPath, pathToMainProject);
536             try {
537                 resolvedPath = mainProject.getCanonicalPath();
538             } catch (IOException e) {
539                 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject);
540                 return;
541             }
542         } else {
543             resolvedPath = mainProject.getAbsolutePath();
544         }
545 
546         println("Resolved location of main project to: %1$s", resolvedPath);
547 
548         // check the main project exists
549         if (checkProjectFolder(resolvedPath) == null) {
550             return;
551         }
552 
553         ProjectProperties props = ProjectProperties.create(folderPath, PropertyType.BUILD);
554 
555         // set or replace the path to the main project
556         props.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject);
557         try {
558             props.save();
559             println("Updated %1$s", PropertyType.BUILD.getFilename());
560         } catch (IOException e) {
561             mLog.error(e, "Failed to write %1$s file in '%2$s'",
562                     PropertyType.BUILD.getFilename(),
563                     folderPath);
564             return;
565         }
566     }
567 
568     /**
569      * Checks whether the give <var>folderPath</var> is a valid project folder, and returns
570      * a {@link File} to the AndroidManifest.xml file.
571      * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it.
572      * <p/>Any error are output using {@link #mLog}.
573      * @param folderPath the folder to check
574      * @return a {@link File} to the AndroidManifest.xml file, or null otherwise.
575      */
checkProjectFolder(String folderPath)576     private File checkProjectFolder(String folderPath) {
577         // project folder must exist and be a directory, since this is an update
578         File projectFolder = new File(folderPath);
579         if (!projectFolder.isDirectory()) {
580             mLog.error(null, "Project folder '%1$s' is not a valid directory, this is not an Android project you can update.",
581                     projectFolder);
582             return null;
583         }
584 
585         // Check AndroidManifest.xml is present
586         File androidManifest = new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML);
587         if (!androidManifest.isFile()) {
588             mLog.error(null,
589                     "%1$s not found in '%2$s', this is not an Android project you can update.",
590                     SdkConstants.FN_ANDROID_MANIFEST_XML,
591                     folderPath);
592             return null;
593         }
594 
595         return androidManifest;
596     }
597 
598     /**
599      * Returns true if any line of the input file contains the requested regexp.
600      */
checkFileContainsRegexp(File file, String regexp)601     private boolean checkFileContainsRegexp(File file, String regexp) {
602         Pattern p = Pattern.compile(regexp);
603 
604         try {
605             BufferedReader in = new BufferedReader(new FileReader(file));
606             String line;
607 
608             while ((line = in.readLine()) != null) {
609                 if (p.matcher(line).find()) {
610                     return true;
611                 }
612             }
613 
614             in.close();
615         } catch (Exception e) {
616             // ignore
617         }
618 
619         return false;
620     }
621 
622     /**
623      * Extracts a "full" package & activity name from an AndroidManifest.xml.
624      * <p/>
625      * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
626      * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}.
627      * When no activity is found, this key is not created.
628      *
629      * @param manifestFile The AndroidManifest.xml file
630      * @param outKeywords  Place where to put the out parameters: package and activity names.
631      * @return True if the package/activity was parsed and updated in the keyword dictionary.
632      */
extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords)633     private boolean extractPackageFromManifest(File manifestFile,
634             Map<String, String> outKeywords) {
635         try {
636             XPath xpath = AndroidXPathFactory.newXPath();
637 
638             InputSource source = new InputSource(new FileReader(manifestFile));
639             String packageName = xpath.evaluate("/manifest/@package", source);
640 
641             source = new InputSource(new FileReader(manifestFile));
642 
643             // Select the "android:name" attribute of all <activity> nodes but only if they
644             // contain a sub-node <intent-filter><action> with an "android:name" attribute which
645             // is 'android.intent.action.MAIN' and an <intent-filter><category> with an
646             // "android:name" attribute which is 'android.intent.category.LAUNCHER'
647             String expression = String.format("/manifest/application/activity" +
648                     "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
649                     "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
650                     "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX);
651 
652             NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
653                     XPathConstants.NODESET);
654 
655             // If we get here, both XPath expressions were valid so we're most likely dealing
656             // with an actual AndroidManifest.xml file. The nodes may not have the requested
657             // attributes though, if which case we should warn.
658 
659             if (packageName == null || packageName.length() == 0) {
660                 mLog.error(null,
661                         "Missing <manifest package=\"...\"> in '%1$s'",
662                         manifestFile.getName());
663                 return false;
664             }
665 
666             // Get the first activity that matched earlier. If there is no activity,
667             // activityName is set to an empty string and the generated "combined" name
668             // will be in the form "package." (with a dot at the end).
669             String activityName = "";
670             if (activityNames.getLength() > 0) {
671                 activityName = activityNames.item(0).getNodeValue();
672             }
673 
674             if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
675                 println("WARNING: There is more than one activity defined in '%1$s'.\n" +
676                         "Only the first one will be used. If this is not appropriate, you need\n" +
677                         "to specify one of these values manually instead:",
678                         manifestFile.getName());
679 
680                 for (int i = 0; i < activityNames.getLength(); i++) {
681                     String name = activityNames.item(i).getNodeValue();
682                     name = combinePackageActivityNames(packageName, name);
683                     println("- %1$s", name);
684                 }
685             }
686 
687             if (activityName.length() == 0) {
688                 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
689                         "No activity will be generated.",
690                         AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName());
691             } else {
692                 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName);
693             }
694 
695             outKeywords.put(PH_PACKAGE, packageName);
696             return true;
697 
698         } catch (IOException e) {
699             mLog.error(e, "Failed to read %1$s", manifestFile.getName());
700         } catch (XPathExpressionException e) {
701             Throwable t = e.getCause();
702             mLog.error(t == null ? e : t,
703                     "Failed to parse %1$s",
704                     manifestFile.getName());
705         }
706 
707         return false;
708     }
709 
combinePackageActivityNames(String packageName, String activityName)710     private String combinePackageActivityNames(String packageName, String activityName) {
711         // Activity Name can have 3 forms:
712         // - ".Name" means this is a class name in the given package name.
713         //    The full FQCN is thus packageName + ".Name"
714         // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
715         // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
716         //   To be valid, the package name should have at least two components. This is checked
717         //   later during the creation of the build.xml file, so we just need to detect there's
718         //   a dot but not at pos==0.
719 
720         int pos = activityName.indexOf('.');
721         if (pos == 0) {
722             return packageName + activityName;
723         } else if (pos > 0) {
724             return activityName;
725         } else {
726             return packageName + "." + activityName;
727         }
728     }
729 
730     /**
731      * Installs a new file that is based on a template file provided by a given target.
732      * Each match of each key from the place-holder map in the template will be replaced with its
733      * corresponding value in the created file.
734      *
735      * @param templateName the name of to the template file
736      * @param destFile the path to the destination file, relative to the project
737      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
738      * @param target the Target of the project that will be providing the template.
739      * @throws ProjectCreateException
740      */
installTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target)741     private void installTemplate(String templateName, File destFile,
742             Map<String, String> placeholderMap, IAndroidTarget target)
743             throws ProjectCreateException {
744         // query the target for its template directory
745         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
746         final String sourcePath = templateFolder + File.separator + templateName;
747 
748         installFullPathTemplate(sourcePath, destFile, placeholderMap);
749     }
750 
751     /**
752      * Installs a new file that is based on a template file provided by the tools folder.
753      * Each match of each key from the place-holder map in the template will be replaced with its
754      * corresponding value in the created file.
755      *
756      * @param templateName the name of to the template file
757      * @param destFile the path to the destination file, relative to the project
758      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
759      * @throws ProjectCreateException
760      */
installTemplate(String templateName, File destFile, Map<String, String> placeholderMap)761     private void installTemplate(String templateName, File destFile,
762             Map<String, String> placeholderMap)
763             throws ProjectCreateException {
764         // query the target for its template directory
765         String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
766         final String sourcePath = templateFolder + File.separator + templateName;
767 
768         installFullPathTemplate(sourcePath, destFile, placeholderMap);
769     }
770 
771     /**
772      * Installs a new file that is based on a template.
773      * Each match of each key from the place-holder map in the template will be replaced with its
774      * corresponding value in the created file.
775      *
776      * @param sourcePath the full path to the source template file
777      * @param destFile the destination file
778      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
779      * @throws ProjectCreateException
780      */
installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap)781     private void installFullPathTemplate(String sourcePath, File destFile,
782             Map<String, String> placeholderMap) throws ProjectCreateException {
783 
784         boolean existed = destFile.exists();
785 
786         try {
787             BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
788             BufferedReader in = new BufferedReader(new FileReader(sourcePath));
789             String line;
790 
791             while ((line = in.readLine()) != null) {
792                 if (placeholderMap != null) {
793                     for (String key : placeholderMap.keySet()) {
794                         line = line.replace(key, placeholderMap.get(key));
795                     }
796                 }
797 
798                 out.write(line);
799                 out.newLine();
800             }
801 
802             out.close();
803             in.close();
804         } catch (Exception e) {
805             throw new ProjectCreateException(e, "Could not access %1$s: %2$s",
806                     destFile, e.getMessage());
807         }
808 
809         println("%1$s file %2$s",
810                 existed ? "Updated" : "Added",
811                 destFile);
812     }
813 
814     /**
815      * Installs the project icons.
816      * @param resourceFolder the resource folder
817      * @param target the target of the project.
818      * @return true if any icon was installed.
819      */
installIcons(File resourceFolder, IAndroidTarget target)820     private boolean installIcons(File resourceFolder, IAndroidTarget target)
821             throws ProjectCreateException {
822         // query the target for its template directory
823         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
824 
825         boolean installedIcon = false;
826 
827         installedIcon |= installIcon(templateFolder, "icon_hdpi.png", resourceFolder, "drawable-hdpi");
828         installedIcon |= installIcon(templateFolder, "icon_mdpi.png", resourceFolder, "drawable-mdpi");
829         installedIcon |= installIcon(templateFolder, "icon_ldpi.png", resourceFolder, "drawable-ldpi");
830 
831         return installedIcon;
832     }
833 
834     /**
835      * Installs an Icon in the project.
836      * @return true if the icon was installed.
837      */
installIcon(String templateFolder, String iconName, File resourceFolder, String folderName)838     private boolean installIcon(String templateFolder, String iconName, File resourceFolder,
839             String folderName) throws ProjectCreateException {
840         File icon = new File(templateFolder, iconName);
841         if (icon.exists()) {
842             File drawable = createDirs(resourceFolder, folderName);
843             installBinaryFile(icon, new File(drawable, "icon.png"));
844             return true;
845         }
846 
847         return false;
848     }
849 
850     /**
851      * Installs a binary file
852      * @param source the source file to copy
853      * @param destination the destination file to write
854      */
installBinaryFile(File source, File destination)855     private void installBinaryFile(File source, File destination) {
856         byte[] buffer = new byte[8192];
857 
858         FileInputStream fis = null;
859         FileOutputStream fos = null;
860         try {
861             fis = new FileInputStream(source);
862             fos = new FileOutputStream(destination);
863 
864             int read;
865             while ((read = fis.read(buffer)) != -1) {
866                 fos.write(buffer, 0, read);
867             }
868 
869         } catch (FileNotFoundException e) {
870             // shouldn't happen since we check before.
871         } catch (IOException e) {
872             new ProjectCreateException(e, "Failed to read binary file: %1$s",
873                     source.getAbsolutePath());
874         } finally {
875             if (fis != null) {
876                 try {
877                     fis.close();
878                 } catch (IOException e) {
879                     // ignore
880                 }
881             }
882             if (fos != null) {
883                 try {
884                     fos.close();
885                 } catch (IOException e) {
886                     // ignore
887                 }
888             }
889         }
890 
891     }
892 
893     /**
894      * Prints a message unless silence is enabled.
895      * <p/>
896      * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
897      * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
898      *
899      * @param format Format for String.format
900      * @param args Arguments for String.format
901      */
println(String format, Object... args)902     private void println(String format, Object... args) {
903         if (mLevel != OutputLevel.SILENT) {
904             if (!format.endsWith("\n")) {
905                 format += "\n";
906             }
907             mLog.printf(format, args);
908         }
909     }
910 
911     /**
912      * Creates a new folder, along with any parent folders that do not exists.
913      *
914      * @param parent the parent folder
915      * @param name the name of the directory to create.
916      * @throws ProjectCreateException
917      */
createDirs(File parent, String name)918     private File createDirs(File parent, String name) throws ProjectCreateException {
919         final File newFolder = new File(parent, name);
920         boolean existedBefore = true;
921 
922         if (!newFolder.exists()) {
923             if (!newFolder.mkdirs()) {
924                 throw new ProjectCreateException("Could not create directory: %1$s", newFolder);
925             }
926             existedBefore = false;
927         }
928 
929         if (newFolder.isDirectory()) {
930             if (!newFolder.canWrite()) {
931                 throw new ProjectCreateException("Path is not writable: %1$s", newFolder);
932             }
933         } else {
934             throw new ProjectCreateException("Path is not a directory: %1$s", newFolder);
935         }
936 
937         if (!existedBefore) {
938             try {
939                 println("Created directory %1$s", newFolder.getCanonicalPath());
940             } catch (IOException e) {
941                 throw new ProjectCreateException(
942                         "Could not determine canonical path of created directory", e);
943             }
944         }
945 
946         return newFolder;
947     }
948 
949     /**
950      * Strips the string of beginning and trailing characters (multiple
951      * characters will be stripped, example stripString("..test...", '.')
952      * results in "test";
953      *
954      * @param s the string to strip
955      * @param strip the character to strip from beginning and end
956      * @return the stripped string or the empty string if everything is stripped.
957      */
stripString(String s, char strip)958     private static String stripString(String s, char strip) {
959         final int sLen = s.length();
960         int newStart = 0, newEnd = sLen - 1;
961 
962         while (newStart < sLen && s.charAt(newStart) == strip) {
963           newStart++;
964         }
965         while (newEnd >= 0 && s.charAt(newEnd) == strip) {
966           newEnd--;
967         }
968 
969         /*
970          * newEnd contains a char we want, and substring takes end as being
971          * exclusive
972          */
973         newEnd++;
974 
975         if (newStart >= sLen || newEnd < 0) {
976             return "";
977         }
978 
979         return s.substring(newStart, newEnd);
980     }
981 }
982