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