• 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_PROJECT_PROGUARD_FILE,
373                     new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE),
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                 if (props == null) {
539                     // This should not happen yet SDK bug 20535 says it can, not sure how.
540                     break;
541                 }
542                 String ref = props.getProperty(propName);
543                 if (ref == null) {
544                     break;
545                 } else {
546                     index++;
547                 }
548             }
549 
550             String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index);
551             propsWC.setProperty(propName, libraryPath);
552             saveProjectProps = true;
553         }
554 
555         // save the default props if needed.
556         if (saveProjectProps) {
557             try {
558                 assert propsWC != null;
559                 propsWC.save();
560                 if (writeProjectProp) {
561                     println("Updated and renamed %1$s to %2$s",
562                             PropertyType.LEGACY_DEFAULT.getFilename(),
563                             PropertyType.PROJECT.getFilename());
564                 } else {
565                     println("Updated %1$s", PropertyType.PROJECT.getFilename());
566                 }
567             } catch (Exception e) {
568                 mLog.error(e, "Failed to write %1$s file in '%2$s'",
569                         PropertyType.PROJECT.getFilename(),
570                         folderPath);
571                 return false;
572             }
573 
574             if (writeProjectProp) {
575                 // need to delete the default prop file.
576                 ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT);
577             }
578         }
579 
580         // Refresh/create "sdk" in local.properties
581         // because the file may already exists and contain other values (like apk config),
582         // we first try to load it.
583         props = ProjectProperties.load(projectFolder, PropertyType.LOCAL);
584         if (props == null) {
585             propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL);
586         } else {
587             propsWC = props.makeWorkingCopy();
588         }
589 
590         // set or replace the sdk location.
591         propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
592         try {
593             propsWC.save();
594             println("Updated %1$s", PropertyType.LOCAL.getFilename());
595         } catch (Exception e) {
596             mLog.error(e, "Failed to write %1$s file in '%2$s'",
597                     PropertyType.LOCAL.getFilename(),
598                     folderPath);
599             return false;
600         }
601 
602         // legacy: check if build.properties must be renamed to ant.properties.
603         props = ProjectProperties.load(projectFolder, PropertyType.ANT);
604         if (props == null) {
605             props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD);
606             if (props != null) {
607                 try {
608                     // get a working copy with the new property type
609                     propsWC = props.makeWorkingCopy(PropertyType.ANT);
610                     propsWC.save();
611 
612                     // delete the old file
613                     ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD);
614 
615                     println("Renamed %1$s to %2$s",
616                             PropertyType.LEGACY_BUILD.getFilename(),
617                             PropertyType.ANT.getFilename());
618                 } catch (Exception e) {
619                     mLog.error(e, "Failed to write %1$s file in '%2$s'",
620                             PropertyType.ANT.getFilename(),
621                             folderPath);
622                     return false;
623                 }
624             }
625         }
626 
627         // Build.xml: create if not present or no <androidinit/> in it
628         File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML);
629         boolean needsBuildXml = projectName != null || !buildXml.exists();
630 
631         // if it seems there's no need for a new build.xml, look for inside the file
632         // to try to detect old ones that may need updating.
633         if (!needsBuildXml) {
634             // we are looking for version-tag: followed by either an integer or "custom".
635             if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$
636                 println("%1$s: Found version-tag: custom. File will not be updated.",
637                         SdkConstants.FN_BUILD_XML);
638             } else {
639                 Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$
640                 if (m == null) {
641                     println("----------\n" +
642                             "%1$s: Failed to find version-tag string. File must be updated.\n" +
643                             "In order to not erase potential customizations, the file will not be automatically regenerated.\n" +
644                             "If no changes have been made to the file, delete it manually and run the command again.\n" +
645                             "If you have made customizations to the build process, the file must be manually updated.\n" +
646                             "It is recommended to:\n" +
647                             "\t* Copy current file to a safe location.\n" +
648                             "\t* Delete original file.\n" +
649                             "\t* Run command again to generate a new file.\n" +
650                             "\t* Port customizations to the new file, by looking at the new rules file\n" +
651                             "\t  located at <SDK>/tools/ant/build.xml\n" +
652                             "\t* Update file to contain\n" +
653                             "\t      version-tag: custom\n" +
654                             "\t  to prevent file from being rewritten automatically by the SDK tools.\n" +
655                             "----------\n",
656                             SdkConstants.FN_BUILD_XML);
657                 } else {
658                     String versionStr = m.group(1);
659                     if (versionStr != null) {
660                         // can't fail due to regexp above.
661                         int version = Integer.parseInt(versionStr);
662                         if (version < MIN_BUILD_VERSION_TAG) {
663                             println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.",
664                                     SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG);
665                             needsBuildXml = true;
666                         }
667                     }
668                 }
669             }
670         }
671 
672         if (needsBuildXml) {
673             // create the map for place-holders of values to replace in the templates
674             final HashMap<String, String> keywords = new HashMap<String, String>();
675 
676             // put the current version-tag value
677             keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG));
678 
679             // if there was no project name on the command line, figure one out.
680             if (projectName == null) {
681                 // otherwise, take it from the existing build.xml if it exists already.
682                 if (buildXml.exists()) {
683                     try {
684                         XPathFactory factory = XPathFactory.newInstance();
685                         XPath xpath = factory.newXPath();
686 
687                         projectName = xpath.evaluate(XPATH_PROJECT_NAME,
688                                 new InputSource(new FileInputStream(buildXml)));
689                     } catch (XPathExpressionException e) {
690                         // this is ok since we're going to recreate the file.
691                         mLog.error(e, "Unable to find existing project name from %1$s",
692                                 SdkConstants.FN_BUILD_XML);
693                     } catch (FileNotFoundException e) {
694                         // can't happen since we check above.
695                     }
696                 }
697 
698                 // if the project is still null, then we find another way.
699                 if (projectName == null) {
700                     extractPackageFromManifest(androidManifest, keywords);
701                     if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) {
702                         String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME);
703                         // keep only the last segment if applicable
704                         int pos = activity.lastIndexOf('.');
705                         if (pos != -1) {
706                             activity = activity.substring(pos + 1);
707                         }
708 
709                         // Use the activity as project name
710                         projectName = activity;
711 
712                         println("No project name specified, using Activity name '%1$s'.\n" +
713                                 "If you wish to change it, edit the first line of %2$s.",
714                                 activity, SdkConstants.FN_BUILD_XML);
715                     } else {
716                         // We need a project name. Just pick up the basename of the project
717                         // directory.
718                         File projectCanonicalFolder = projectFolder;
719                         try {
720                             projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile();
721                         } catch (IOException e) {
722                             // ignore, keep going
723                         }
724 
725                         // Use the folder name as project name
726                         projectName = projectCanonicalFolder.getName();
727 
728                         println("No project name specified, using project folder name '%1$s'.\n" +
729                                 "If you wish to change it, edit the first line of %2$s.",
730                                 projectName, SdkConstants.FN_BUILD_XML);
731                     }
732                 }
733             }
734 
735             // put the project name in the map for replacement during the template installation.
736             keywords.put(PH_PROJECT_NAME, projectName);
737 
738             if (mLevel == OutputLevel.VERBOSE) {
739                 println("Regenerating %1$s with project name %2$s",
740                         SdkConstants.FN_BUILD_XML,
741                         keywords.get(PH_PROJECT_NAME));
742             }
743 
744             try {
745                 installTemplate("build.template", buildXml, keywords);
746             } catch (ProjectCreateException e) {
747                 mLog.error(e, null);
748                 return false;
749             }
750         }
751 
752         if (hasProguard == false) {
753             try {
754                 installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE,
755                         // Write ProGuard config files with the extension .pro which
756                         // is what is used in the ProGuard documentation and samples
757                         new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE),
758                         null /*placeholderMap*/);
759             } catch (ProjectCreateException e) {
760                 mLog.error(e, null);
761                 return false;
762             }
763         }
764 
765         return true;
766     }
767 
768     /**
769      * Updates a test project with a new path to the main (tested) project.
770      * @param folderPath the path of the test project.
771      * @param pathToMainProject the path to the main project, relative to the test project.
772      */
773     @SuppressWarnings("deprecation")
updateTestProject(final String folderPath, final String pathToMainProject, final SdkManager sdkManager)774     public void updateTestProject(final String folderPath, final String pathToMainProject,
775             final SdkManager sdkManager) {
776         // since this is an update, check the folder does point to a project
777         if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
778             return;
779         }
780 
781         // check the path to the main project is valid.
782         File mainProject = new File(pathToMainProject);
783         String resolvedPath;
784         if (mainProject.isAbsolute() == false) {
785             mainProject = new File(folderPath, pathToMainProject);
786             try {
787                 resolvedPath = mainProject.getCanonicalPath();
788             } catch (IOException e) {
789                 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject);
790                 return;
791             }
792         } else {
793             resolvedPath = mainProject.getAbsolutePath();
794         }
795 
796         println("Resolved location of main project to: %1$s", resolvedPath);
797 
798         // check the main project exists
799         if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
800             mLog.error(null, "No Android Manifest at: %1$s", resolvedPath);
801             return;
802         }
803 
804         // now get the target from the main project
805         ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT);
806         if (projectProp == null) {
807             // legacy support for older file name.
808             projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT);
809             if (projectProp == null) {
810                 mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(),
811                         resolvedPath);
812                 return;
813             }
814         }
815 
816         String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET);
817         if (targetHash == null) {
818             mLog.error(null, "%1$s in the main project has no target property.",
819                     PropertyType.PROJECT.getFilename());
820             return;
821         }
822 
823         IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash);
824         if (target == null) {
825             mLog.error(null, "Main project target %1$s is not a valid target.", targetHash);
826             return;
827         }
828 
829         // update test-project does not support the --name parameter, therefore the project
830         // name should generally not be passed to updateProject().
831         // However if build.xml does not exist then updateProject() will recreate it. In this
832         // case we will need the project name.
833         // To do this, we look for the parent project name and add "test" to it.
834         // If the main project does not have a project name (yet), then the default behavior
835         // will be used (look for activity and then folder name)
836         String projectName = null;
837         XPathFactory factory = XPathFactory.newInstance();
838         XPath xpath = factory.newXPath();
839 
840         File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML);
841         if (testBuildXml.isFile() == false) {
842             File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML);
843             if (mainBuildXml.isFile()) {
844                 try {
845                     // get the name of the main project and add Test to it.
846                     String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME,
847                             new InputSource(new FileInputStream(mainBuildXml)));
848                     projectName = mainProjectName + "Test";
849                 } catch (XPathExpressionException e) {
850                     // it's ok, updateProject() will figure out a name automatically.
851                     // We do log the error though as the build.xml file may be broken.
852                     mLog.warning("Failed to parse %1$s.\n" +
853                             "File may not be valid. Consider running 'android update project' on the main project.",
854                             mainBuildXml.getPath());
855                 } catch (FileNotFoundException e) {
856                     // should not happen since we check first.
857                 }
858             }
859         }
860 
861         // now update the project as if it's a normal project
862         if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) {
863             // error message has already been displayed.
864             return;
865         }
866 
867         // add the test project specific properties.
868         // At this point, we know build.prop has been renamed ant.prop
869         ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT);
870         ProjectPropertiesWorkingCopy antWorkingCopy;
871         if (antProps == null) {
872             antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT);
873         } else {
874             antWorkingCopy = antProps.makeWorkingCopy();
875         }
876 
877         // set or replace the path to the main project
878         antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject);
879         try {
880             antWorkingCopy.save();
881             println("Updated %1$s", PropertyType.ANT.getFilename());
882         } catch (Exception e) {
883             mLog.error(e, "Failed to write %1$s file in '%2$s'",
884                     PropertyType.ANT.getFilename(),
885                     folderPath);
886             return;
887         }
888     }
889 
890     /**
891      * Checks whether the give <var>folderPath</var> is a valid project folder, and returns
892      * a {@link FileWrapper} to the required file.
893      * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it.
894      * <p/>Any error are output using {@link #mLog}.
895      * @param folderPath the folder to check
896      * @param requiredFilename the file name of the file that's required.
897      * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise.
898      */
checkProjectFolder(String folderPath, String requiredFilename)899     private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) {
900         // project folder must exist and be a directory, since this is an update
901         FolderWrapper projectFolder = new FolderWrapper(folderPath);
902         if (!projectFolder.isDirectory()) {
903             mLog.error(null, "Project folder '%1$s' is not a valid directory.",
904                     projectFolder);
905             return null;
906         }
907 
908         // Check AndroidManifest.xml is present
909         FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename);
910         if (!requireFile.isFile()) {
911             mLog.error(null,
912                     "%1$s is not a valid project (%2$s not found).",
913                     folderPath, requiredFilename);
914             return null;
915         }
916 
917         return requireFile;
918     }
919 
920     /**
921      * Looks for a given regex in a file and returns the matcher if any line of the input file
922      * contains the requested regexp.
923      *
924      * @param file the file to search.
925      * @param regexp the regexp to search for.
926      *
927      * @return a Matcher or null if the regexp is not found.
928      */
checkFileContainsRegexp(File file, String regexp)929     private Matcher checkFileContainsRegexp(File file, String regexp) {
930         Pattern p = Pattern.compile(regexp);
931 
932         try {
933             BufferedReader in = new BufferedReader(new FileReader(file));
934             String line;
935 
936             while ((line = in.readLine()) != null) {
937                 Matcher m = p.matcher(line);
938                 if (m.find()) {
939                     return m;
940                 }
941             }
942 
943             in.close();
944         } catch (Exception e) {
945             // ignore
946         }
947 
948         return null;
949     }
950 
951     /**
952      * Extracts a "full" package & activity name from an AndroidManifest.xml.
953      * <p/>
954      * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
955      * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}.
956      * When no activity is found, this key is not created.
957      *
958      * @param manifestFile The AndroidManifest.xml file
959      * @param outKeywords  Place where to put the out parameters: package and activity names.
960      * @return True if the package/activity was parsed and updated in the keyword dictionary.
961      */
extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords)962     private boolean extractPackageFromManifest(File manifestFile,
963             Map<String, String> outKeywords) {
964         try {
965             XPath xpath = AndroidXPathFactory.newXPath();
966 
967             InputSource source = new InputSource(new FileReader(manifestFile));
968             String packageName = xpath.evaluate("/manifest/@package", source);
969 
970             source = new InputSource(new FileReader(manifestFile));
971 
972             // Select the "android:name" attribute of all <activity> nodes but only if they
973             // contain a sub-node <intent-filter><action> with an "android:name" attribute which
974             // is 'android.intent.action.MAIN' and an <intent-filter><category> with an
975             // "android:name" attribute which is 'android.intent.category.LAUNCHER'
976             String expression = String.format("/manifest/application/activity" +
977                     "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
978                     "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
979                     "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX);
980 
981             NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
982                     XPathConstants.NODESET);
983 
984             // If we get here, both XPath expressions were valid so we're most likely dealing
985             // with an actual AndroidManifest.xml file. The nodes may not have the requested
986             // attributes though, if which case we should warn.
987 
988             if (packageName == null || packageName.length() == 0) {
989                 mLog.error(null,
990                         "Missing <manifest package=\"...\"> in '%1$s'",
991                         manifestFile.getName());
992                 return false;
993             }
994 
995             // Get the first activity that matched earlier. If there is no activity,
996             // activityName is set to an empty string and the generated "combined" name
997             // will be in the form "package." (with a dot at the end).
998             String activityName = "";
999             if (activityNames.getLength() > 0) {
1000                 activityName = activityNames.item(0).getNodeValue();
1001             }
1002 
1003             if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
1004                 println("WARNING: There is more than one activity defined in '%1$s'.\n" +
1005                         "Only the first one will be used. If this is not appropriate, you need\n" +
1006                         "to specify one of these values manually instead:",
1007                         manifestFile.getName());
1008 
1009                 for (int i = 0; i < activityNames.getLength(); i++) {
1010                     String name = activityNames.item(i).getNodeValue();
1011                     name = combinePackageActivityNames(packageName, name);
1012                     println("- %1$s", name);
1013                 }
1014             }
1015 
1016             if (activityName.length() == 0) {
1017                 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
1018                         "No activity will be generated.",
1019                         AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName());
1020             } else {
1021                 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName);
1022             }
1023 
1024             outKeywords.put(PH_PACKAGE, packageName);
1025             return true;
1026 
1027         } catch (IOException e) {
1028             mLog.error(e, "Failed to read %1$s", manifestFile.getName());
1029         } catch (XPathExpressionException e) {
1030             Throwable t = e.getCause();
1031             mLog.error(t == null ? e : t,
1032                     "Failed to parse %1$s",
1033                     manifestFile.getName());
1034         }
1035 
1036         return false;
1037     }
1038 
combinePackageActivityNames(String packageName, String activityName)1039     private String combinePackageActivityNames(String packageName, String activityName) {
1040         // Activity Name can have 3 forms:
1041         // - ".Name" means this is a class name in the given package name.
1042         //    The full FQCN is thus packageName + ".Name"
1043         // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
1044         // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
1045         //   To be valid, the package name should have at least two components. This is checked
1046         //   later during the creation of the build.xml file, so we just need to detect there's
1047         //   a dot but not at pos==0.
1048 
1049         int pos = activityName.indexOf('.');
1050         if (pos == 0) {
1051             return packageName + activityName;
1052         } else if (pos > 0) {
1053             return activityName;
1054         } else {
1055             return packageName + "." + activityName;
1056         }
1057     }
1058 
1059     /**
1060      * Installs a new file that is based on a template file provided by a given target.
1061      * Each match of each key from the place-holder map in the template will be replaced with its
1062      * corresponding value in the created file.
1063      *
1064      * @param templateName the name of to the template file
1065      * @param destFile the path to the destination file, relative to the project
1066      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
1067      * @param target the Target of the project that will be providing the template.
1068      * @throws ProjectCreateException
1069      */
installTargetTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target)1070     private void installTargetTemplate(String templateName, File destFile,
1071             Map<String, String> placeholderMap, IAndroidTarget target)
1072             throws ProjectCreateException {
1073         // query the target for its template directory
1074         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
1075         final String sourcePath = templateFolder + File.separator + templateName;
1076 
1077         installFullPathTemplate(sourcePath, destFile, placeholderMap);
1078     }
1079 
1080     /**
1081      * Installs a new file that is based on a template file provided by the tools folder.
1082      * Each match of each key from the place-holder map in the template will be replaced with its
1083      * corresponding value in the created file.
1084      *
1085      * @param templateName the name of to the template file
1086      * @param destFile the path to the destination file, relative to the project
1087      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
1088      * @throws ProjectCreateException
1089      */
installTemplate(String templateName, File destFile, Map<String, String> placeholderMap)1090     private void installTemplate(String templateName, File destFile,
1091             Map<String, String> placeholderMap)
1092             throws ProjectCreateException {
1093         // query the target for its template directory
1094         String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
1095         final String sourcePath = templateFolder + File.separator + templateName;
1096 
1097         installFullPathTemplate(sourcePath, destFile, placeholderMap);
1098     }
1099 
1100     /**
1101      * Installs a new file that is based on a template.
1102      * Each match of each key from the place-holder map in the template will be replaced with its
1103      * corresponding value in the created file.
1104      *
1105      * @param sourcePath the full path to the source template file
1106      * @param destFile the destination file
1107      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
1108      * @throws ProjectCreateException
1109      */
installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap)1110     private void installFullPathTemplate(String sourcePath, File destFile,
1111             Map<String, String> placeholderMap) throws ProjectCreateException {
1112 
1113         boolean existed = destFile.exists();
1114 
1115         try {
1116             BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
1117             BufferedReader in = new BufferedReader(new FileReader(sourcePath));
1118             String line;
1119 
1120             while ((line = in.readLine()) != null) {
1121                 if (placeholderMap != null) {
1122                     for (Map.Entry<String, String> entry : placeholderMap.entrySet()) {
1123                         line = line.replace(entry.getKey(), entry.getValue());
1124                     }
1125                 }
1126 
1127                 out.write(line);
1128                 out.newLine();
1129             }
1130 
1131             out.close();
1132             in.close();
1133         } catch (Exception e) {
1134             throw new ProjectCreateException(e, "Could not access %1$s: %2$s",
1135                     destFile, e.getMessage());
1136         }
1137 
1138         println("%1$s file %2$s",
1139                 existed ? "Updated" : "Added",
1140                 destFile);
1141     }
1142 
1143     /**
1144      * Installs the project icons.
1145      * @param resourceFolder the resource folder
1146      * @param target the target of the project.
1147      * @return true if any icon was installed.
1148      */
installIcons(File resourceFolder, IAndroidTarget target)1149     private boolean installIcons(File resourceFolder, IAndroidTarget target)
1150             throws ProjectCreateException {
1151         // query the target for its template directory
1152         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
1153 
1154         boolean installedIcon = false;
1155 
1156         installedIcon |= installIcon(templateFolder, "ic_launcher_xhdpi.png", resourceFolder,
1157                 "drawable-xhdpi");
1158         installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder,
1159                 "drawable-hdpi");
1160         installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder,
1161                 "drawable-mdpi");
1162         installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder,
1163                 "drawable-ldpi");
1164 
1165         return installedIcon;
1166     }
1167 
1168     /**
1169      * Installs an Icon in the project.
1170      * @return true if the icon was installed.
1171      */
installIcon(String templateFolder, String iconName, File resourceFolder, String folderName)1172     private boolean installIcon(String templateFolder, String iconName, File resourceFolder,
1173             String folderName) throws ProjectCreateException {
1174         File icon = new File(templateFolder, iconName);
1175         if (icon.exists()) {
1176             File drawable = createDirs(resourceFolder, folderName);
1177             installBinaryFile(icon, new File(drawable, "ic_launcher.png"));
1178             return true;
1179         }
1180 
1181         return false;
1182     }
1183 
1184     /**
1185      * Installs a binary file
1186      * @param source the source file to copy
1187      * @param destination the destination file to write
1188      * @throws ProjectCreateException
1189      */
installBinaryFile(File source, File destination)1190     private void installBinaryFile(File source, File destination) throws ProjectCreateException {
1191         byte[] buffer = new byte[8192];
1192 
1193         FileInputStream fis = null;
1194         FileOutputStream fos = null;
1195         try {
1196             fis = new FileInputStream(source);
1197             fos = new FileOutputStream(destination);
1198 
1199             int read;
1200             while ((read = fis.read(buffer)) != -1) {
1201                 fos.write(buffer, 0, read);
1202             }
1203 
1204         } catch (FileNotFoundException e) {
1205             // shouldn't happen since we check before.
1206         } catch (IOException e) {
1207             throw new ProjectCreateException(e, "Failed to read binary file: %1$s",
1208                     source.getAbsolutePath());
1209         } finally {
1210             if (fis != null) {
1211                 try {
1212                     fis.close();
1213                 } catch (IOException e) {
1214                     // ignore
1215                 }
1216             }
1217             if (fos != null) {
1218                 try {
1219                     fos.close();
1220                 } catch (IOException e) {
1221                     // ignore
1222                 }
1223             }
1224         }
1225 
1226     }
1227 
1228     /**
1229      * Prints a message unless silence is enabled.
1230      * <p/>
1231      * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
1232      * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
1233      *
1234      * @param format Format for String.format
1235      * @param args Arguments for String.format
1236      */
println(String format, Object... args)1237     private void println(String format, Object... args) {
1238         if (mLevel != OutputLevel.SILENT) {
1239             if (!format.endsWith("\n")) {
1240                 format += "\n";
1241             }
1242             mLog.printf(format, args);
1243         }
1244     }
1245 
1246     /**
1247      * Creates a new folder, along with any parent folders that do not exists.
1248      *
1249      * @param parent the parent folder
1250      * @param name the name of the directory to create.
1251      * @throws ProjectCreateException
1252      */
createDirs(File parent, String name)1253     private File createDirs(File parent, String name) throws ProjectCreateException {
1254         final File newFolder = new File(parent, name);
1255         boolean existedBefore = true;
1256 
1257         if (!newFolder.exists()) {
1258             if (!newFolder.mkdirs()) {
1259                 throw new ProjectCreateException("Could not create directory: %1$s", newFolder);
1260             }
1261             existedBefore = false;
1262         }
1263 
1264         if (newFolder.isDirectory()) {
1265             if (!newFolder.canWrite()) {
1266                 throw new ProjectCreateException("Path is not writable: %1$s", newFolder);
1267             }
1268         } else {
1269             throw new ProjectCreateException("Path is not a directory: %1$s", newFolder);
1270         }
1271 
1272         if (!existedBefore) {
1273             try {
1274                 println("Created directory %1$s", newFolder.getCanonicalPath());
1275             } catch (IOException e) {
1276                 throw new ProjectCreateException(
1277                         "Could not determine canonical path of created directory", e);
1278             }
1279         }
1280 
1281         return newFolder;
1282     }
1283 
1284     /**
1285      * Strips the string of beginning and trailing characters (multiple
1286      * characters will be stripped, example stripString("..test...", '.')
1287      * results in "test";
1288      *
1289      * @param s the string to strip
1290      * @param strip the character to strip from beginning and end
1291      * @return the stripped string or the empty string if everything is stripped.
1292      */
stripString(String s, char strip)1293     private static String stripString(String s, char strip) {
1294         final int sLen = s.length();
1295         int newStart = 0, newEnd = sLen - 1;
1296 
1297         while (newStart < sLen && s.charAt(newStart) == strip) {
1298           newStart++;
1299         }
1300         while (newEnd >= 0 && s.charAt(newEnd) == strip) {
1301           newEnd--;
1302         }
1303 
1304         /*
1305          * newEnd contains a char we want, and substring takes end as being
1306          * exclusive
1307          */
1308         newEnd++;
1309 
1310         if (newStart >= sLen || newEnd < 0) {
1311             return "";
1312         }
1313 
1314         return s.substring(newStart, newEnd);
1315     }
1316 }
1317