• 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.AndroidConstants;
20 import com.android.io.FileWrapper;
21 import com.android.io.FolderWrapper;
22 import com.android.sdklib.IAndroidTarget;
23 import com.android.sdklib.ISdkLog;
24 import com.android.sdklib.SdkConstants;
25 import com.android.sdklib.SdkManager;
26 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
27 import com.android.sdklib.xml.AndroidManifest;
28 import com.android.sdklib.xml.AndroidXPathFactory;
29 
30 import org.w3c.dom.NodeList;
31 import org.xml.sax.InputSource;
32 
33 import java.io.BufferedReader;
34 import java.io.BufferedWriter;
35 import java.io.File;
36 import java.io.FileInputStream;
37 import java.io.FileNotFoundException;
38 import java.io.FileOutputStream;
39 import java.io.FileReader;
40 import java.io.FileWriter;
41 import java.io.IOException;
42 import java.util.HashMap;
43 import java.util.Map;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46 
47 import javax.xml.xpath.XPath;
48 import javax.xml.xpath.XPathConstants;
49 import javax.xml.xpath.XPathExpressionException;
50 import javax.xml.xpath.XPathFactory;
51 
52 /**
53  * Creates the basic files needed to get an Android project up and running.
54  *
55  * @hide
56  */
57 public class ProjectCreator {
58 
59     /** Version of the build.xml. Stored in version-tag */
60     private final static int MIN_BUILD_VERSION_TAG = 1;
61 
62     /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */
63     private final static String PH_JAVA_FOLDER = "PACKAGE_PATH";
64     /** Package name substitution string used in template files, i.e. "PACKAGE" */
65     private final static String PH_PACKAGE = "PACKAGE";
66     /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME".
67      * @deprecated This is only used for older templates. For new ones see
68      * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */
69     @Deprecated
70     private final static String PH_ACTIVITY_NAME = "ACTIVITY_NAME";
71     /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/
72     private final static String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME";
73     /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/
74     private final static String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME";
75     /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/
76     private final static String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME";
77     /** Original Activity class name substitution string used in class templates, i.e.
78      * "ACTIVITY_TESTED_CLASS_NAME".*/
79     private final static String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME";
80     /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */
81     private final static String PH_PROJECT_NAME = "PROJECT_NAME";
82     /** Application icon substitution string used in the manifest template */
83     private final static String PH_ICON = "ICON";
84     /** Version tag name substitution string used in template files, i.e. "VERSION_TAG". */
85     private final static String PH_VERSION_TAG = "VERSION_TAG";
86 
87     /** The xpath to find a project name in an Ant build file. */
88     private static final String XPATH_PROJECT_NAME = "/project/@name";
89 
90     /** Pattern for characters accepted in a project name. Since this will be used as a
91      * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */
92     public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+");
93     /** List of valid characters for a project name. Used for display purposes. */
94     public final static String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _";
95 
96     /** Pattern for characters accepted in a package name. A package is list of Java identifier
97      * separated by a dot. We need to have at least one dot (e.g. a two-level package name).
98      * A Java identifier cannot start by a digit. */
99     public static final Pattern RE_PACKAGE_NAME =
100         Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+");
101     /** List of valid characters for a project name. Used for display purposes. */
102     public final static String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _";
103 
104     /** Pattern for characters accepted in an activity name, which is a Java identifier. */
105     public static final Pattern RE_ACTIVITY_NAME =
106         Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
107     /** List of valid characters for a project name. Used for display purposes. */
108     public final static String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _";
109 
110 
111     public enum OutputLevel {
112         /** Silent mode. Project creation will only display errors. */
113         SILENT,
114         /** Normal mode. Project creation will display what's being done, display
115          * error but not warnings. */
116         NORMAL,
117         /** Verbose mode. Project creation will display what's being done, errors and warnings. */
118         VERBOSE;
119     }
120 
121     /**
122      * Exception thrown when a project creation fails, typically because a template
123      * file cannot be written.
124      */
125     private static class ProjectCreateException extends Exception {
126         /** default UID. This will not be serialized anyway. */
127         private static final long serialVersionUID = 1L;
128 
129         @SuppressWarnings("unused")
ProjectCreateException(String message)130         ProjectCreateException(String message) {
131             super(message);
132         }
133 
ProjectCreateException(Throwable t, String format, Object... args)134         ProjectCreateException(Throwable t, String format, Object... args) {
135             super(format != null ? String.format(format, args) : format, t);
136         }
137 
ProjectCreateException(String format, Object... args)138         ProjectCreateException(String format, Object... args) {
139             super(String.format(format, args));
140         }
141     }
142 
143     /** The {@link OutputLevel} verbosity. */
144     private final OutputLevel mLevel;
145     /** Logger for errors and output. Cannot be null. */
146     private final ISdkLog mLog;
147     /** The OS path of the SDK folder. */
148     private final String mSdkFolder;
149     /** The {@link SdkManager} instance. */
150     private final SdkManager mSdkManager;
151 
152     /**
153      * Helper class to create android projects.
154      *
155      * @param sdkManager The {@link SdkManager} instance.
156      * @param sdkFolder The OS path of the SDK folder.
157      * @param level The {@link OutputLevel} verbosity.
158      * @param log Logger for errors and output. Cannot be null.
159      */
ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ISdkLog log)160     public ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ISdkLog log) {
161         mSdkManager = sdkManager;
162         mSdkFolder = sdkFolder;
163         mLevel = level;
164         mLog = log;
165     }
166 
167     /**
168      * Creates a new project.
169      * <p/>
170      * The caller should have already checked and sanitized the parameters.
171      *
172      * @param folderPath the folder of the project to create.
173      * @param projectName the name of the project. The name must match the
174      *          {@link #RE_PROJECT_NAME} regex.
175      * @param packageName the package of the project. The name must match the
176      *          {@link #RE_PACKAGE_NAME} regex.
177      * @param activityEntry the activity of the project as it will appear in the manifest. Can be
178      *          null if no activity should be created. The name must match the
179      *          {@link #RE_ACTIVITY_NAME} regex.
180      * @param target the project target.
181      * @param library whether the project is a library.
182      * @param pathToMainProject if non-null the project will be setup to test a main project
183      * located at the given path.
184      */
createProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, boolean library, String pathToMainProject)185     public void createProject(String folderPath, String projectName,
186             String packageName, String activityEntry, IAndroidTarget target, boolean library,
187             String pathToMainProject) {
188 
189         // create project folder if it does not exist
190         File projectFolder = checkNewProjectLocation(folderPath);
191         if (projectFolder == null) {
192             return;
193         }
194 
195         try {
196             boolean isTestProject = pathToMainProject != null;
197 
198             // first create the project properties.
199 
200             // location of the SDK goes in localProperty
201             ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath,
202                     PropertyType.LOCAL);
203             localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
204             localProperties.save();
205 
206             // target goes in default properties
207             ProjectPropertiesWorkingCopy defaultProperties = ProjectProperties.create(folderPath,
208                     PropertyType.PROJECT);
209             defaultProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
210             if (library) {
211                 defaultProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true");
212             }
213             defaultProperties.save();
214 
215             // create a build.properties file with just the application package
216             ProjectPropertiesWorkingCopy buildProperties = ProjectProperties.create(folderPath,
217                     PropertyType.ANT);
218 
219             if (isTestProject) {
220                 buildProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT,
221                         pathToMainProject);
222             }
223 
224             buildProperties.save();
225 
226             // create the map for place-holders of values to replace in the templates
227             final HashMap<String, String> keywords = new HashMap<String, String>();
228 
229             // create the required folders.
230             // compute src folder path
231             final String packagePath =
232                 stripString(packageName.replace(".", File.separator),
233                         File.separatorChar);
234 
235             // put this path in the place-holder map for project files that needs to list
236             // files manually.
237             keywords.put(PH_JAVA_FOLDER, packagePath);
238             keywords.put(PH_PACKAGE, packageName);
239             keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG));
240 
241 
242             // compute some activity related information
243             String fqActivityName = null, activityPath = null, activityClassName = null;
244             String originalActivityEntry = activityEntry;
245             String originalActivityClassName = null;
246             if (activityEntry != null) {
247                 if (isTestProject) {
248                     // append Test so that it doesn't collide with the main project activity.
249                     activityEntry += "Test";
250 
251                     // get the classname from the original activity entry.
252                     int pos = originalActivityEntry.lastIndexOf('.');
253                     if (pos != -1) {
254                         originalActivityClassName = originalActivityEntry.substring(pos + 1);
255                     } else {
256                         originalActivityClassName = originalActivityEntry;
257                     }
258                 }
259 
260                 // get the fully qualified name of the activity
261                 fqActivityName = AndroidManifest.combinePackageAndClassName(packageName,
262                         activityEntry);
263 
264                 // get the activity path (replace the . to /)
265                 activityPath = stripString(fqActivityName.replace(".", File.separator),
266                         File.separatorChar);
267 
268                 // remove the last segment, so that we only have the path to the activity, but
269                 // not the activity filename itself.
270                 activityPath = activityPath.substring(0,
271                         activityPath.lastIndexOf(File.separatorChar));
272 
273                 // finally, get the class name for the activity
274                 activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1);
275             }
276 
277             // at this point we have the following for the activity:
278             // activityEntry: this is the manifest entry. For instance .MyActivity
279             // fqActivityName: full-qualified class name: com.foo.MyActivity
280             // activityClassName: only the classname: MyActivity
281             // originalActivityClassName: the classname of the activity being tested (if applicable)
282 
283             // Add whatever activity info is needed in the place-holder map.
284             // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests).
285             if (target.getVersion().getApiLevel() < 4) { // legacy
286                 if (originalActivityEntry != null) {
287                     keywords.put(PH_ACTIVITY_NAME, originalActivityEntry);
288                 }
289             } else {
290                 // newer templates make a difference between the manifest entries, classnames,
291                 // as well as the main and test classes.
292                 if (activityEntry != null) {
293                     keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry);
294                     keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName);
295                     keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName);
296                     if (originalActivityClassName != null) {
297                         keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName);
298                     }
299                 }
300             }
301 
302             // Take the project name from the command line if there's one
303             if (projectName != null) {
304                 keywords.put(PH_PROJECT_NAME, projectName);
305             } else {
306                 if (activityClassName != null) {
307                     // Use the activity class name as project name
308                     keywords.put(PH_PROJECT_NAME, activityClassName);
309                 } else {
310                     // We need a project name. Just pick up the basename of the project
311                     // directory.
312                     projectName = projectFolder.getName();
313                     keywords.put(PH_PROJECT_NAME, projectName);
314                 }
315             }
316 
317             // create the source folder for the activity
318             if (activityClassName != null) {
319                 String srcActivityFolderPath =
320                         SdkConstants.FD_SOURCES + File.separator + activityPath;
321                 File sourceFolder = createDirs(projectFolder, srcActivityFolderPath);
322 
323                 String javaTemplate = isTestProject ? "java_tests_file.template"
324                         : "java_file.template";
325                 String activityFileName = activityClassName + ".java";
326 
327                 installTargetTemplate(javaTemplate, new File(sourceFolder, activityFileName),
328                         keywords, target);
329             } else {
330                 // we should at least create 'src'
331                 createDirs(projectFolder, SdkConstants.FD_SOURCES);
332             }
333 
334             // create other useful folders
335             File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES);
336             createDirs(projectFolder, SdkConstants.FD_OUTPUT);
337             createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS);
338 
339             if (isTestProject == false) {
340                 /* Make res files only for non test projects */
341                 File valueFolder = createDirs(resourceFolder, AndroidConstants.FD_RES_VALUES);
342                 installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"),
343                         keywords, target);
344 
345                 File layoutFolder = createDirs(resourceFolder, AndroidConstants.FD_RES_LAYOUT);
346                 installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"),
347                         keywords, target);
348 
349                 // create the icons
350                 if (installIcons(resourceFolder, target)) {
351                     keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\"");
352                 } else {
353                     keywords.put(PH_ICON, "");
354                 }
355             }
356 
357             /* Make AndroidManifest.xml and build.xml files */
358             String manifestTemplate = "AndroidManifest.template";
359             if (isTestProject) {
360                 manifestTemplate = "AndroidManifest.tests.template";
361             }
362 
363             installTargetTemplate(manifestTemplate,
364                     new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML),
365                     keywords, target);
366 
367             installTemplate("build.template",
368                     new File(projectFolder, SdkConstants.FN_BUILD_XML),
369                     keywords);
370 
371             // install the proguard config file.
372             installTemplate(SdkConstants.FN_PROGUARD_CFG,
373                     new File(projectFolder, SdkConstants.FN_PROGUARD_CFG),
374                     null /*keywords*/);
375         } catch (Exception e) {
376             mLog.error(e, null);
377         }
378     }
379 
checkNewProjectLocation(String folderPath)380     private File checkNewProjectLocation(String folderPath) {
381         File projectFolder = new File(folderPath);
382         if (!projectFolder.exists()) {
383 
384             boolean created = false;
385             Throwable t = null;
386             try {
387                 created = projectFolder.mkdirs();
388             } catch (Exception e) {
389                 t = e;
390             }
391 
392             if (created) {
393                 println("Created project directory: %1$s", projectFolder);
394             } else {
395                 mLog.error(t, "Could not create directory: %1$s", projectFolder);
396                 return null;
397             }
398         } else {
399             Exception e = null;
400             String error = null;
401             try {
402                 String[] content = projectFolder.list();
403                 if (content == null) {
404                     error = "Project folder '%1$s' is not a directory.";
405                 } else if (content.length != 0) {
406                     error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead.";
407                 }
408             } catch (Exception e1) {
409                 e = e1;
410             }
411 
412             if (e != null || error != null) {
413                 mLog.error(e, error, projectFolder, SdkConstants.androidCmdName());
414             }
415         }
416         return projectFolder;
417     }
418 
419     /**
420      * Updates an existing project.
421      * <p/>
422      * Workflow:
423      * <ul>
424      * <li> Check AndroidManifest.xml is present (required)
425      * <li> Check if there's a legacy properties file and convert it
426      * <li> Check there's a project.properties with a target *or* --target was specified
427      * <li> Update default.prop if --target was specified
428      * <li> Refresh/create "sdk" in local.properties
429      * <li> Build.xml: create if not present or if version-tag is found or not. version-tag:custom
430      * prevent any overwrite. version-tag:[integer] will override. missing version-tag will query
431      * the dev.
432      * </ul>
433      *
434      * @param folderPath the folder of the project to update. This folder must exist.
435      * @param target the project target. Can be null.
436      * @param projectName The project name from --name. Can be null.
437      * @param libraryPath the path to a library to add to the references. Can be null.
438      * @return true if the project was successfully updated.
439      */
440     @SuppressWarnings("deprecation")
updateProject(String folderPath, IAndroidTarget target, String projectName, String libraryPath)441     public boolean updateProject(String folderPath, IAndroidTarget target, String projectName,
442             String libraryPath) {
443         // since this is an update, check the folder does point to a project
444         FileWrapper androidManifest = checkProjectFolder(folderPath,
445                 SdkConstants.FN_ANDROID_MANIFEST_XML);
446         if (androidManifest == null) {
447             return false;
448         }
449 
450         // get the parent folder.
451         FolderWrapper projectFolder = (FolderWrapper) androidManifest.getParentFolder();
452 
453         boolean hasProguard = false;
454 
455         // Check there's a project.properties with a target *or* --target was specified
456         IAndroidTarget originalTarget = null;
457         boolean writeProjectProp = false;
458         ProjectProperties props = ProjectProperties.load(projectFolder, PropertyType.PROJECT);
459 
460         if (props == null) {
461             // no project.properties, try to load default.properties
462             props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_DEFAULT);
463             writeProjectProp = true;
464         }
465 
466         if (props != null) {
467             String targetHash = props.getProperty(ProjectProperties.PROPERTY_TARGET);
468             originalTarget = mSdkManager.getTargetFromHashString(targetHash);
469 
470             // if the project is already setup with proguard, we won't copy the proguard config.
471             hasProguard = props.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null;
472         }
473 
474         if (originalTarget == null && target == null) {
475             mLog.error(null,
476                 "The project either has no target set or the target is invalid.\n" +
477                 "Please provide a --target to the '%1$s update' command.",
478                 SdkConstants.androidCmdName());
479             return false;
480         }
481 
482         boolean saveProjectProps = false;
483 
484         ProjectPropertiesWorkingCopy propsWC = null;
485 
486         // Update default.prop if --target was specified
487         if (target != null || writeProjectProp) {
488             // we already attempted to load the file earlier, if that failed, create it.
489             if (props == null) {
490                 propsWC = ProjectProperties.create(projectFolder, PropertyType.PROJECT);
491             } else {
492                 propsWC = props.makeWorkingCopy(PropertyType.PROJECT);
493             }
494 
495             // set or replace the target
496             if (target != null) {
497                 propsWC.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
498             }
499             saveProjectProps = true;
500         }
501 
502         if (libraryPath != null) {
503             // At this point, the default properties already exists, either because they were
504             // already there or because they were created with a new target
505             if (propsWC == null) {
506                 assert props != null;
507                 propsWC = props.makeWorkingCopy();
508             }
509 
510             // check the reference is valid
511             File libProject = new File(libraryPath);
512             String resolvedPath;
513             if (libProject.isAbsolute() == false) {
514                 libProject = new File(projectFolder, libraryPath);
515                 try {
516                     resolvedPath = libProject.getCanonicalPath();
517                 } catch (IOException e) {
518                     mLog.error(e, "Unable to resolve path to library project: %1$s", libraryPath);
519                     return false;
520                 }
521             } else {
522                 resolvedPath = libProject.getAbsolutePath();
523             }
524 
525             println("Resolved location of library project to: %1$s", resolvedPath);
526 
527             // check the lib project exists
528             if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
529                 mLog.error(null, "No Android Manifest at: %1$s", resolvedPath);
530                 return false;
531             }
532 
533             // look for other references to figure out the index
534             int index = 1;
535             while (true) {
536                 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index);
537                 assert props != null;
538                 String ref = props.getProperty(propName);
539                 if (ref == null) {
540                     break;
541                 } else {
542                     index++;
543                 }
544             }
545 
546             String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index);
547             propsWC.setProperty(propName, libraryPath);
548             saveProjectProps = true;
549         }
550 
551         // save the default props if needed.
552         if (saveProjectProps) {
553             try {
554                 assert propsWC != null;
555                 propsWC.save();
556                 if (writeProjectProp) {
557                     println("Updated and renamed %1$s to %2$s",
558                             PropertyType.LEGACY_DEFAULT.getFilename(),
559                             PropertyType.PROJECT.getFilename());
560                 } else {
561                     println("Updated %1$s", PropertyType.PROJECT.getFilename());
562                 }
563             } catch (Exception e) {
564                 mLog.error(e, "Failed to write %1$s file in '%2$s'",
565                         PropertyType.PROJECT.getFilename(),
566                         folderPath);
567                 return false;
568             }
569 
570             if (writeProjectProp) {
571                 // need to delete the default prop file.
572                 ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT);
573             }
574         }
575 
576         // Refresh/create "sdk" in local.properties
577         // because the file may already exists and contain other values (like apk config),
578         // we first try to load it.
579         props = ProjectProperties.load(projectFolder, PropertyType.LOCAL);
580         if (props == null) {
581             propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL);
582         } else {
583             propsWC = props.makeWorkingCopy();
584         }
585 
586         // set or replace the sdk location.
587         propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
588         try {
589             propsWC.save();
590             println("Updated %1$s", PropertyType.LOCAL.getFilename());
591         } catch (Exception e) {
592             mLog.error(e, "Failed to write %1$s file in '%2$s'",
593                     PropertyType.LOCAL.getFilename(),
594                     folderPath);
595             return false;
596         }
597 
598         // legacy: check if build.properties must be renamed to ant.properties.
599         props = ProjectProperties.load(projectFolder, PropertyType.ANT);
600         if (props == null) {
601             props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD);
602             if (props != null) {
603                 try {
604                     // get a working copy with the new property type
605                     propsWC = props.makeWorkingCopy(PropertyType.ANT);
606                     propsWC.save();
607 
608                     // delete the old file
609                     ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD);
610 
611                     println("Renamed %1$s to %2$s",
612                             PropertyType.LEGACY_BUILD.getFilename(),
613                             PropertyType.ANT.getFilename());
614                 } catch (Exception e) {
615                     mLog.error(e, "Failed to write %1$s file in '%2$s'",
616                             PropertyType.ANT.getFilename(),
617                             folderPath);
618                     return false;
619                 }
620             }
621         }
622 
623         // Build.xml: create if not present or no <androidinit/> in it
624         File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML);
625         boolean needsBuildXml = projectName != null || !buildXml.exists();
626 
627         // if it seems there's no need for a new build.xml, look for inside the file
628         // to try to detect old ones that may need updating.
629         if (!needsBuildXml) {
630             // we are looking for version-tag: followed by either an integer or "custom".
631             if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$
632                 println("%1$s: Found version-tag: custom. File will not be updated.",
633                         SdkConstants.FN_BUILD_XML);
634             } else {
635                 Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$
636                 if (m == null) {
637                     println("----------\n" +
638                             "%1$s: Failed to find version-tag string. File must be updated.\n" +
639                             "In order to not erase potential customizations, the file will not be automatically regenerated.\n" +
640                             "If no changes have been made to the file, delete it manually and run the command again.\n" +
641                             "If you have made customizations to the build process, the file must be manually updated.\n" +
642                             "It is recommended to:\n" +
643                             "\t* Copy current file to a safe location.\n" +
644                             "\t* Delete original file.\n" +
645                             "\t* Run command again to generate a new file.\n" +
646                             "\t* Port customizations to the new file, by looking at the new rules file\n" +
647                             "\t  located at <SDK>/tools/ant/build.xml\n" +
648                             "\t* Update file to contain\n" +
649                             "\t      version-tag: custom\n" +
650                             "\t  to prevent file from being rewritten automatically by the SDK tools.\n" +
651                             "----------\n",
652                             SdkConstants.FN_BUILD_XML);
653                 } else {
654                     String versionStr = m.group(1);
655                     if (versionStr != null) {
656                         // can't fail due to regexp above.
657                         int version = Integer.parseInt(versionStr);
658                         if (version < MIN_BUILD_VERSION_TAG) {
659                             println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.",
660                                     SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG);
661                             needsBuildXml = true;
662                         }
663                     }
664                 }
665             }
666         }
667 
668         if (needsBuildXml) {
669             // create the map for place-holders of values to replace in the templates
670             final HashMap<String, String> keywords = new HashMap<String, String>();
671 
672             // put the current version-tag value
673             keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG));
674 
675             // if there was no project name on the command line, figure one out.
676             if (projectName == null) {
677                 // otherwise, take it from the existing build.xml if it exists already.
678                 if (buildXml.exists()) {
679                     try {
680                         XPathFactory factory = XPathFactory.newInstance();
681                         XPath xpath = factory.newXPath();
682 
683                         projectName = xpath.evaluate(XPATH_PROJECT_NAME,
684                                 new InputSource(new FileInputStream(buildXml)));
685                     } catch (XPathExpressionException e) {
686                         // this is ok since we're going to recreate the file.
687                         mLog.error(e, "Unable to find existing project name from %1$s",
688                                 SdkConstants.FN_BUILD_XML);
689                     } catch (FileNotFoundException e) {
690                         // can't happen since we check above.
691                     }
692                 }
693 
694                 // if the project is still null, then we find another way.
695                 if (projectName == null) {
696                     extractPackageFromManifest(androidManifest, keywords);
697                     if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) {
698                         String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME);
699                         // keep only the last segment if applicable
700                         int pos = activity.lastIndexOf('.');
701                         if (pos != -1) {
702                             activity = activity.substring(pos + 1);
703                         }
704 
705                         // Use the activity as project name
706                         projectName = activity;
707 
708                         println("No project name specified, using Activity name '%1$s'.\n" +
709                                 "If you wish to change it, edit the first line of %2$s.",
710                                 activity, SdkConstants.FN_BUILD_XML);
711                     } else {
712                         // We need a project name. Just pick up the basename of the project
713                         // directory.
714                         File projectCanonicalFolder = projectFolder;
715                         try {
716                             projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile();
717                         } catch (IOException e) {
718                             // ignore, keep going
719                         }
720 
721                         // Use the folder name as project name
722                         projectName = projectCanonicalFolder.getName();
723 
724                         println("No project name specified, using project folder name '%1$s'.\n" +
725                                 "If you wish to change it, edit the first line of %2$s.",
726                                 projectName, SdkConstants.FN_BUILD_XML);
727                     }
728                 }
729             }
730 
731             // put the project name in the map for replacement during the template installation.
732             keywords.put(PH_PROJECT_NAME, projectName);
733 
734             if (mLevel == OutputLevel.VERBOSE) {
735                 println("Regenerating %1$s with project name %2$s",
736                         SdkConstants.FN_BUILD_XML,
737                         keywords.get(PH_PROJECT_NAME));
738             }
739 
740             try {
741                 installTemplate("build.template", buildXml, keywords);
742             } catch (ProjectCreateException e) {
743                 mLog.error(e, null);
744                 return false;
745             }
746         }
747 
748         if (hasProguard == false) {
749             try {
750                 installTemplate(SdkConstants.FN_PROGUARD_CFG,
751                         new File(projectFolder, SdkConstants.FN_PROGUARD_CFG),
752                         null /*placeholderMap*/);
753             } catch (ProjectCreateException e) {
754                 mLog.error(e, null);
755                 return false;
756             }
757         }
758 
759         return true;
760     }
761 
762     /**
763      * Updates a test project with a new path to the main (tested) project.
764      * @param folderPath the path of the test project.
765      * @param pathToMainProject the path to the main project, relative to the test project.
766      */
767     @SuppressWarnings("deprecation")
updateTestProject(final String folderPath, final String pathToMainProject, final SdkManager sdkManager)768     public void updateTestProject(final String folderPath, final String pathToMainProject,
769             final SdkManager sdkManager) {
770         // since this is an update, check the folder does point to a project
771         if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
772             return;
773         }
774 
775         // check the path to the main project is valid.
776         File mainProject = new File(pathToMainProject);
777         String resolvedPath;
778         if (mainProject.isAbsolute() == false) {
779             mainProject = new File(folderPath, pathToMainProject);
780             try {
781                 resolvedPath = mainProject.getCanonicalPath();
782             } catch (IOException e) {
783                 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject);
784                 return;
785             }
786         } else {
787             resolvedPath = mainProject.getAbsolutePath();
788         }
789 
790         println("Resolved location of main project to: %1$s", resolvedPath);
791 
792         // check the main project exists
793         if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
794             mLog.error(null, "No Android Manifest at: %1$s", resolvedPath);
795             return;
796         }
797 
798         // now get the target from the main project
799         ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT);
800         if (projectProp == null) {
801             // legacy support for older file name.
802             projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT);
803             if (projectProp == null) {
804                 mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(),
805                         resolvedPath);
806                 return;
807             }
808         }
809 
810         String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET);
811         if (targetHash == null) {
812             mLog.error(null, "%1$s in the main project has no target property.",
813                     PropertyType.PROJECT.getFilename());
814             return;
815         }
816 
817         IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash);
818         if (target == null) {
819             mLog.error(null, "Main project target %1$s is not a valid target.", targetHash);
820             return;
821         }
822 
823         // update test-project does not support the --name parameter, therefore the project
824         // name should generally not be passed to updateProject().
825         // However if build.xml does not exist then updateProject() will recreate it. In this
826         // case we will need the project name.
827         // To do this, we look for the parent project name and add "test" to it.
828         // If the main project does not have a project name (yet), then the default behavior
829         // will be used (look for activity and then folder name)
830         String projectName = null;
831         XPathFactory factory = XPathFactory.newInstance();
832         XPath xpath = factory.newXPath();
833 
834         File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML);
835         if (testBuildXml.isFile() == false) {
836             File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML);
837             if (mainBuildXml.isFile()) {
838                 try {
839                     // get the name of the main project and add Test to it.
840                     String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME,
841                             new InputSource(new FileInputStream(mainBuildXml)));
842                     projectName = mainProjectName + "Test";
843                 } catch (XPathExpressionException e) {
844                     // it's ok, updateProject() will figure out a name automatically.
845                     // We do log the error though as the build.xml file may be broken.
846                     mLog.warning("Failed to parse %1$s.\n" +
847                             "File may not be valid. Consider running 'android update project' on the main project.",
848                             mainBuildXml.getPath());
849                 } catch (FileNotFoundException e) {
850                     // should not happen since we check first.
851                 }
852             }
853         }
854 
855         // now update the project as if it's a normal project
856         if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) {
857             // error message has already been displayed.
858             return;
859         }
860 
861         // add the test project specific properties.
862         // At this point, we know build.prop has been renamed ant.prop
863         ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT);
864         ProjectPropertiesWorkingCopy antWorkingCopy;
865         if (antProps == null) {
866             antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT);
867         } else {
868             antWorkingCopy = antProps.makeWorkingCopy();
869         }
870 
871         // set or replace the path to the main project
872         antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject);
873         try {
874             antWorkingCopy.save();
875             println("Updated %1$s", PropertyType.ANT.getFilename());
876         } catch (Exception e) {
877             mLog.error(e, "Failed to write %1$s file in '%2$s'",
878                     PropertyType.ANT.getFilename(),
879                     folderPath);
880             return;
881         }
882     }
883 
884     /**
885      * Checks whether the give <var>folderPath</var> is a valid project folder, and returns
886      * a {@link FileWrapper} to the required file.
887      * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it.
888      * <p/>Any error are output using {@link #mLog}.
889      * @param folderPath the folder to check
890      * @param requiredFilename the file name of the file that's required.
891      * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise.
892      */
checkProjectFolder(String folderPath, String requiredFilename)893     private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) {
894         // project folder must exist and be a directory, since this is an update
895         FolderWrapper projectFolder = new FolderWrapper(folderPath);
896         if (!projectFolder.isDirectory()) {
897             mLog.error(null, "Project folder '%1$s' is not a valid directory.",
898                     projectFolder);
899             return null;
900         }
901 
902         // Check AndroidManifest.xml is present
903         FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename);
904         if (!requireFile.isFile()) {
905             mLog.error(null,
906                     "%1$s is not a valid project (%2$s not found).",
907                     folderPath, requiredFilename);
908             return null;
909         }
910 
911         return requireFile;
912     }
913 
914     /**
915      * Looks for a given regex in a file and returns the matcher if any line of the input file
916      * contains the requested regexp.
917      *
918      * @param file the file to search.
919      * @param regexp the regexp to search for.
920      *
921      * @return a Matcher or null if the regexp is not found.
922      */
checkFileContainsRegexp(File file, String regexp)923     private Matcher checkFileContainsRegexp(File file, String regexp) {
924         Pattern p = Pattern.compile(regexp);
925 
926         try {
927             BufferedReader in = new BufferedReader(new FileReader(file));
928             String line;
929 
930             while ((line = in.readLine()) != null) {
931                 Matcher m = p.matcher(line);
932                 if (m.find()) {
933                     return m;
934                 }
935             }
936 
937             in.close();
938         } catch (Exception e) {
939             // ignore
940         }
941 
942         return null;
943     }
944 
945     /**
946      * Extracts a "full" package & activity name from an AndroidManifest.xml.
947      * <p/>
948      * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
949      * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}.
950      * When no activity is found, this key is not created.
951      *
952      * @param manifestFile The AndroidManifest.xml file
953      * @param outKeywords  Place where to put the out parameters: package and activity names.
954      * @return True if the package/activity was parsed and updated in the keyword dictionary.
955      */
extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords)956     private boolean extractPackageFromManifest(File manifestFile,
957             Map<String, String> outKeywords) {
958         try {
959             XPath xpath = AndroidXPathFactory.newXPath();
960 
961             InputSource source = new InputSource(new FileReader(manifestFile));
962             String packageName = xpath.evaluate("/manifest/@package", source);
963 
964             source = new InputSource(new FileReader(manifestFile));
965 
966             // Select the "android:name" attribute of all <activity> nodes but only if they
967             // contain a sub-node <intent-filter><action> with an "android:name" attribute which
968             // is 'android.intent.action.MAIN' and an <intent-filter><category> with an
969             // "android:name" attribute which is 'android.intent.category.LAUNCHER'
970             String expression = String.format("/manifest/application/activity" +
971                     "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
972                     "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
973                     "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX);
974 
975             NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
976                     XPathConstants.NODESET);
977 
978             // If we get here, both XPath expressions were valid so we're most likely dealing
979             // with an actual AndroidManifest.xml file. The nodes may not have the requested
980             // attributes though, if which case we should warn.
981 
982             if (packageName == null || packageName.length() == 0) {
983                 mLog.error(null,
984                         "Missing <manifest package=\"...\"> in '%1$s'",
985                         manifestFile.getName());
986                 return false;
987             }
988 
989             // Get the first activity that matched earlier. If there is no activity,
990             // activityName is set to an empty string and the generated "combined" name
991             // will be in the form "package." (with a dot at the end).
992             String activityName = "";
993             if (activityNames.getLength() > 0) {
994                 activityName = activityNames.item(0).getNodeValue();
995             }
996 
997             if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
998                 println("WARNING: There is more than one activity defined in '%1$s'.\n" +
999                         "Only the first one will be used. If this is not appropriate, you need\n" +
1000                         "to specify one of these values manually instead:",
1001                         manifestFile.getName());
1002 
1003                 for (int i = 0; i < activityNames.getLength(); i++) {
1004                     String name = activityNames.item(i).getNodeValue();
1005                     name = combinePackageActivityNames(packageName, name);
1006                     println("- %1$s", name);
1007                 }
1008             }
1009 
1010             if (activityName.length() == 0) {
1011                 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
1012                         "No activity will be generated.",
1013                         AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName());
1014             } else {
1015                 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName);
1016             }
1017 
1018             outKeywords.put(PH_PACKAGE, packageName);
1019             return true;
1020 
1021         } catch (IOException e) {
1022             mLog.error(e, "Failed to read %1$s", manifestFile.getName());
1023         } catch (XPathExpressionException e) {
1024             Throwable t = e.getCause();
1025             mLog.error(t == null ? e : t,
1026                     "Failed to parse %1$s",
1027                     manifestFile.getName());
1028         }
1029 
1030         return false;
1031     }
1032 
combinePackageActivityNames(String packageName, String activityName)1033     private String combinePackageActivityNames(String packageName, String activityName) {
1034         // Activity Name can have 3 forms:
1035         // - ".Name" means this is a class name in the given package name.
1036         //    The full FQCN is thus packageName + ".Name"
1037         // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
1038         // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
1039         //   To be valid, the package name should have at least two components. This is checked
1040         //   later during the creation of the build.xml file, so we just need to detect there's
1041         //   a dot but not at pos==0.
1042 
1043         int pos = activityName.indexOf('.');
1044         if (pos == 0) {
1045             return packageName + activityName;
1046         } else if (pos > 0) {
1047             return activityName;
1048         } else {
1049             return packageName + "." + activityName;
1050         }
1051     }
1052 
1053     /**
1054      * Installs a new file that is based on a template file provided by a given target.
1055      * Each match of each key from the place-holder map in the template will be replaced with its
1056      * corresponding value in the created file.
1057      *
1058      * @param templateName the name of to the template file
1059      * @param destFile the path to the destination file, relative to the project
1060      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
1061      * @param target the Target of the project that will be providing the template.
1062      * @throws ProjectCreateException
1063      */
installTargetTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target)1064     private void installTargetTemplate(String templateName, File destFile,
1065             Map<String, String> placeholderMap, IAndroidTarget target)
1066             throws ProjectCreateException {
1067         // query the target for its template directory
1068         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
1069         final String sourcePath = templateFolder + File.separator + templateName;
1070 
1071         installFullPathTemplate(sourcePath, destFile, placeholderMap);
1072     }
1073 
1074     /**
1075      * Installs a new file that is based on a template file provided by the tools folder.
1076      * Each match of each key from the place-holder map in the template will be replaced with its
1077      * corresponding value in the created file.
1078      *
1079      * @param templateName the name of to the template file
1080      * @param destFile the path to the destination file, relative to the project
1081      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
1082      * @throws ProjectCreateException
1083      */
installTemplate(String templateName, File destFile, Map<String, String> placeholderMap)1084     private void installTemplate(String templateName, File destFile,
1085             Map<String, String> placeholderMap)
1086             throws ProjectCreateException {
1087         // query the target for its template directory
1088         String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
1089         final String sourcePath = templateFolder + File.separator + templateName;
1090 
1091         installFullPathTemplate(sourcePath, destFile, placeholderMap);
1092     }
1093 
1094     /**
1095      * Installs a new file that is based on a template.
1096      * Each match of each key from the place-holder map in the template will be replaced with its
1097      * corresponding value in the created file.
1098      *
1099      * @param sourcePath the full path to the source template file
1100      * @param destFile the destination file
1101      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
1102      * @throws ProjectCreateException
1103      */
installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap)1104     private void installFullPathTemplate(String sourcePath, File destFile,
1105             Map<String, String> placeholderMap) throws ProjectCreateException {
1106 
1107         boolean existed = destFile.exists();
1108 
1109         try {
1110             BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
1111             BufferedReader in = new BufferedReader(new FileReader(sourcePath));
1112             String line;
1113 
1114             while ((line = in.readLine()) != null) {
1115                 if (placeholderMap != null) {
1116                     for (Map.Entry<String, String> entry : placeholderMap.entrySet()) {
1117                         line = line.replace(entry.getKey(), entry.getValue());
1118                     }
1119                 }
1120 
1121                 out.write(line);
1122                 out.newLine();
1123             }
1124 
1125             out.close();
1126             in.close();
1127         } catch (Exception e) {
1128             throw new ProjectCreateException(e, "Could not access %1$s: %2$s",
1129                     destFile, e.getMessage());
1130         }
1131 
1132         println("%1$s file %2$s",
1133                 existed ? "Updated" : "Added",
1134                 destFile);
1135     }
1136 
1137     /**
1138      * Installs the project icons.
1139      * @param resourceFolder the resource folder
1140      * @param target the target of the project.
1141      * @return true if any icon was installed.
1142      */
installIcons(File resourceFolder, IAndroidTarget target)1143     private boolean installIcons(File resourceFolder, IAndroidTarget target)
1144             throws ProjectCreateException {
1145         // query the target for its template directory
1146         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
1147 
1148         boolean installedIcon = false;
1149 
1150         installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder,
1151                 "drawable-hdpi");
1152         installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder,
1153                 "drawable-mdpi");
1154         installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder,
1155                 "drawable-ldpi");
1156 
1157         return installedIcon;
1158     }
1159 
1160     /**
1161      * Installs an Icon in the project.
1162      * @return true if the icon was installed.
1163      */
installIcon(String templateFolder, String iconName, File resourceFolder, String folderName)1164     private boolean installIcon(String templateFolder, String iconName, File resourceFolder,
1165             String folderName) throws ProjectCreateException {
1166         File icon = new File(templateFolder, iconName);
1167         if (icon.exists()) {
1168             File drawable = createDirs(resourceFolder, folderName);
1169             installBinaryFile(icon, new File(drawable, "ic_launcher.png"));
1170             return true;
1171         }
1172 
1173         return false;
1174     }
1175 
1176     /**
1177      * Installs a binary file
1178      * @param source the source file to copy
1179      * @param destination the destination file to write
1180      * @throws ProjectCreateException
1181      */
installBinaryFile(File source, File destination)1182     private void installBinaryFile(File source, File destination) throws ProjectCreateException {
1183         byte[] buffer = new byte[8192];
1184 
1185         FileInputStream fis = null;
1186         FileOutputStream fos = null;
1187         try {
1188             fis = new FileInputStream(source);
1189             fos = new FileOutputStream(destination);
1190 
1191             int read;
1192             while ((read = fis.read(buffer)) != -1) {
1193                 fos.write(buffer, 0, read);
1194             }
1195 
1196         } catch (FileNotFoundException e) {
1197             // shouldn't happen since we check before.
1198         } catch (IOException e) {
1199             throw new ProjectCreateException(e, "Failed to read binary file: %1$s",
1200                     source.getAbsolutePath());
1201         } finally {
1202             if (fis != null) {
1203                 try {
1204                     fis.close();
1205                 } catch (IOException e) {
1206                     // ignore
1207                 }
1208             }
1209             if (fos != null) {
1210                 try {
1211                     fos.close();
1212                 } catch (IOException e) {
1213                     // ignore
1214                 }
1215             }
1216         }
1217 
1218     }
1219 
1220     /**
1221      * Prints a message unless silence is enabled.
1222      * <p/>
1223      * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
1224      * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
1225      *
1226      * @param format Format for String.format
1227      * @param args Arguments for String.format
1228      */
println(String format, Object... args)1229     private void println(String format, Object... args) {
1230         if (mLevel != OutputLevel.SILENT) {
1231             if (!format.endsWith("\n")) {
1232                 format += "\n";
1233             }
1234             mLog.printf(format, args);
1235         }
1236     }
1237 
1238     /**
1239      * Creates a new folder, along with any parent folders that do not exists.
1240      *
1241      * @param parent the parent folder
1242      * @param name the name of the directory to create.
1243      * @throws ProjectCreateException
1244      */
createDirs(File parent, String name)1245     private File createDirs(File parent, String name) throws ProjectCreateException {
1246         final File newFolder = new File(parent, name);
1247         boolean existedBefore = true;
1248 
1249         if (!newFolder.exists()) {
1250             if (!newFolder.mkdirs()) {
1251                 throw new ProjectCreateException("Could not create directory: %1$s", newFolder);
1252             }
1253             existedBefore = false;
1254         }
1255 
1256         if (newFolder.isDirectory()) {
1257             if (!newFolder.canWrite()) {
1258                 throw new ProjectCreateException("Path is not writable: %1$s", newFolder);
1259             }
1260         } else {
1261             throw new ProjectCreateException("Path is not a directory: %1$s", newFolder);
1262         }
1263 
1264         if (!existedBefore) {
1265             try {
1266                 println("Created directory %1$s", newFolder.getCanonicalPath());
1267             } catch (IOException e) {
1268                 throw new ProjectCreateException(
1269                         "Could not determine canonical path of created directory", e);
1270             }
1271         }
1272 
1273         return newFolder;
1274     }
1275 
1276     /**
1277      * Strips the string of beginning and trailing characters (multiple
1278      * characters will be stripped, example stripString("..test...", '.')
1279      * results in "test";
1280      *
1281      * @param s the string to strip
1282      * @param strip the character to strip from beginning and end
1283      * @return the stripped string or the empty string if everything is stripped.
1284      */
stripString(String s, char strip)1285     private static String stripString(String s, char strip) {
1286         final int sLen = s.length();
1287         int newStart = 0, newEnd = sLen - 1;
1288 
1289         while (newStart < sLen && s.charAt(newStart) == strip) {
1290           newStart++;
1291         }
1292         while (newEnd >= 0 && s.charAt(newEnd) == strip) {
1293           newEnd--;
1294         }
1295 
1296         /*
1297          * newEnd contains a char we want, and substring takes end as being
1298          * exclusive
1299          */
1300         newEnd++;
1301 
1302         if (newStart >= sLen || newEnd < 0) {
1303             return "";
1304         }
1305 
1306         return s.substring(newStart, newEnd);
1307     }
1308 }
1309