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.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")
    130         ProjectCreateException(String message) {
    131             super(message);
    132         }
    133 
    134         ProjectCreateException(Throwable t, String format, Object... args) {
    135             super(format != null ? String.format(format, args) : format, t);
    136         }
    137 
    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      */
    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      */
    185     public void createProject(String folderPath, String projectName,
    186             String packageName, String activityEntry, IAndroidTarget target, boolean library,
    187             String pathToMainProject) {
    188 
    189         // create project folder if it does not exist
    190         File projectFolder = checkNewProjectLocation(folderPath);
    191         if (projectFolder == null) {
    192             return;
    193         }
    194 
    195         try {
    196             boolean isTestProject = pathToMainProject != null;
    197 
    198             // first create the project properties.
    199 
    200             // location of the SDK goes in localProperty
    201             ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath,
    202                     PropertyType.LOCAL);
    203             localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
    204             localProperties.save();
    205 
    206             // target goes in default properties
    207             ProjectPropertiesWorkingCopy defaultProperties = ProjectProperties.create(folderPath,
    208                     PropertyType.PROJECT);
    209             defaultProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
    210             if (library) {
    211                 defaultProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true");
    212             }
    213             defaultProperties.save();
    214 
    215             // create a build.properties file with just the application package
    216             ProjectPropertiesWorkingCopy buildProperties = ProjectProperties.create(folderPath,
    217                     PropertyType.ANT);
    218 
    219             if (isTestProject) {
    220                 buildProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT,
    221                         pathToMainProject);
    222             }
    223 
    224             buildProperties.save();
    225 
    226             // create the map for place-holders of values to replace in the templates
    227             final HashMap<String, String> keywords = new HashMap<String, String>();
    228 
    229             // create the required folders.
    230             // compute src folder path
    231             final String packagePath =
    232                 stripString(packageName.replace(".", File.separator),
    233                         File.separatorChar);
    234 
    235             // put this path in the place-holder map for project files that needs to list
    236             // files manually.
    237             keywords.put(PH_JAVA_FOLDER, packagePath);
    238             keywords.put(PH_PACKAGE, packageName);
    239             keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG));
    240 
    241 
    242             // compute some activity related information
    243             String fqActivityName = null, activityPath = null, activityClassName = null;
    244             String originalActivityEntry = activityEntry;
    245             String originalActivityClassName = null;
    246             if (activityEntry != null) {
    247                 if (isTestProject) {
    248                     // append Test so that it doesn't collide with the main project activity.
    249                     activityEntry += "Test";
    250 
    251                     // get the classname from the original activity entry.
    252                     int pos = originalActivityEntry.lastIndexOf('.');
    253                     if (pos != -1) {
    254                         originalActivityClassName = originalActivityEntry.substring(pos + 1);
    255                     } else {
    256                         originalActivityClassName = originalActivityEntry;
    257                     }
    258                 }
    259 
    260                 // get the fully qualified name of the activity
    261                 fqActivityName = AndroidManifest.combinePackageAndClassName(packageName,
    262                         activityEntry);
    263 
    264                 // get the activity path (replace the . to /)
    265                 activityPath = stripString(fqActivityName.replace(".", File.separator),
    266                         File.separatorChar);
    267 
    268                 // remove the last segment, so that we only have the path to the activity, but
    269                 // not the activity filename itself.
    270                 activityPath = activityPath.substring(0,
    271                         activityPath.lastIndexOf(File.separatorChar));
    272 
    273                 // finally, get the class name for the activity
    274                 activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1);
    275             }
    276 
    277             // at this point we have the following for the activity:
    278             // activityEntry: this is the manifest entry. For instance .MyActivity
    279             // fqActivityName: full-qualified class name: com.foo.MyActivity
    280             // activityClassName: only the classname: MyActivity
    281             // originalActivityClassName: the classname of the activity being tested (if applicable)
    282 
    283             // Add whatever activity info is needed in the place-holder map.
    284             // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests).
    285             if (target.getVersion().getApiLevel() < 4) { // legacy
    286                 if (originalActivityEntry != null) {
    287                     keywords.put(PH_ACTIVITY_NAME, originalActivityEntry);
    288                 }
    289             } else {
    290                 // newer templates make a difference between the manifest entries, classnames,
    291                 // as well as the main and test classes.
    292                 if (activityEntry != null) {
    293                     keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry);
    294                     keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName);
    295                     keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName);
    296                     if (originalActivityClassName != null) {
    297                         keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName);
    298                     }
    299                 }
    300             }
    301 
    302             // Take the project name from the command line if there's one
    303             if (projectName != null) {
    304                 keywords.put(PH_PROJECT_NAME, projectName);
    305             } else {
    306                 if (activityClassName != null) {
    307                     // Use the activity class name as project name
    308                     keywords.put(PH_PROJECT_NAME, activityClassName);
    309                 } else {
    310                     // We need a project name. Just pick up the basename of the project
    311                     // directory.
    312                     projectName = projectFolder.getName();
    313                     keywords.put(PH_PROJECT_NAME, projectName);
    314                 }
    315             }
    316 
    317             // create the source folder for the activity
    318             if (activityClassName != null) {
    319                 String srcActivityFolderPath =
    320                         SdkConstants.FD_SOURCES + File.separator + activityPath;
    321                 File sourceFolder = createDirs(projectFolder, srcActivityFolderPath);
    322 
    323                 String javaTemplate = isTestProject ? "java_tests_file.template"
    324                         : "java_file.template";
    325                 String activityFileName = activityClassName + ".java";
    326 
    327                 installTargetTemplate(javaTemplate, new File(sourceFolder, activityFileName),
    328                         keywords, target);
    329             } else {
    330                 // we should at least create 'src'
    331                 createDirs(projectFolder, SdkConstants.FD_SOURCES);
    332             }
    333 
    334             // create other useful folders
    335             File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES);
    336             createDirs(projectFolder, SdkConstants.FD_OUTPUT);
    337             createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS);
    338 
    339             if (isTestProject == false) {
    340                 /* Make res files only for non test projects */
    341                 File valueFolder = createDirs(resourceFolder, AndroidConstants.FD_RES_VALUES);
    342                 installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"),
    343                         keywords, target);
    344 
    345                 File layoutFolder = createDirs(resourceFolder, AndroidConstants.FD_RES_LAYOUT);
    346                 installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"),
    347                         keywords, target);
    348 
    349                 // create the icons
    350                 if (installIcons(resourceFolder, target)) {
    351                     keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\"");
    352                 } else {
    353                     keywords.put(PH_ICON, "");
    354                 }
    355             }
    356 
    357             /* Make AndroidManifest.xml and build.xml files */
    358             String manifestTemplate = "AndroidManifest.template";
    359             if (isTestProject) {
    360                 manifestTemplate = "AndroidManifest.tests.template";
    361             }
    362 
    363             installTargetTemplate(manifestTemplate,
    364                     new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML),
    365                     keywords, target);
    366 
    367             installTemplate("build.template",
    368                     new File(projectFolder, SdkConstants.FN_BUILD_XML),
    369                     keywords);
    370 
    371             // install the proguard config file.
    372             installTemplate(SdkConstants.FN_PROGUARD_CFG,
    373                     new File(projectFolder, SdkConstants.FN_PROGUARD_CFG),
    374                     null /*keywords*/);
    375         } catch (Exception e) {
    376             mLog.error(e, null);
    377         }
    378     }
    379 
    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")
    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_PROGUARD_CFG,
    755                         new File(projectFolder, SdkConstants.FN_PROGUARD_CFG),
    756                         null /*placeholderMap*/);
    757             } catch (ProjectCreateException e) {
    758                 mLog.error(e, null);
    759                 return false;
    760             }
    761         }
    762 
    763         return true;
    764     }
    765 
    766     /**
    767      * Updates a test project with a new path to the main (tested) project.
    768      * @param folderPath the path of the test project.
    769      * @param pathToMainProject the path to the main project, relative to the test project.
    770      */
    771     @SuppressWarnings("deprecation")
    772     public void updateTestProject(final String folderPath, final String pathToMainProject,
    773             final SdkManager sdkManager) {
    774         // since this is an update, check the folder does point to a project
    775         if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
    776             return;
    777         }
    778 
    779         // check the path to the main project is valid.
    780         File mainProject = new File(pathToMainProject);
    781         String resolvedPath;
    782         if (mainProject.isAbsolute() == false) {
    783             mainProject = new File(folderPath, pathToMainProject);
    784             try {
    785                 resolvedPath = mainProject.getCanonicalPath();
    786             } catch (IOException e) {
    787                 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject);
    788                 return;
    789             }
    790         } else {
    791             resolvedPath = mainProject.getAbsolutePath();
    792         }
    793 
    794         println("Resolved location of main project to: %1$s", resolvedPath);
    795 
    796         // check the main project exists
    797         if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) {
    798             mLog.error(null, "No Android Manifest at: %1$s", resolvedPath);
    799             return;
    800         }
    801 
    802         // now get the target from the main project
    803         ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT);
    804         if (projectProp == null) {
    805             // legacy support for older file name.
    806             projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT);
    807             if (projectProp == null) {
    808                 mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(),
    809                         resolvedPath);
    810                 return;
    811             }
    812         }
    813 
    814         String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET);
    815         if (targetHash == null) {
    816             mLog.error(null, "%1$s in the main project has no target property.",
    817                     PropertyType.PROJECT.getFilename());
    818             return;
    819         }
    820 
    821         IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash);
    822         if (target == null) {
    823             mLog.error(null, "Main project target %1$s is not a valid target.", targetHash);
    824             return;
    825         }
    826 
    827         // update test-project does not support the --name parameter, therefore the project
    828         // name should generally not be passed to updateProject().
    829         // However if build.xml does not exist then updateProject() will recreate it. In this
    830         // case we will need the project name.
    831         // To do this, we look for the parent project name and add "test" to it.
    832         // If the main project does not have a project name (yet), then the default behavior
    833         // will be used (look for activity and then folder name)
    834         String projectName = null;
    835         XPathFactory factory = XPathFactory.newInstance();
    836         XPath xpath = factory.newXPath();
    837 
    838         File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML);
    839         if (testBuildXml.isFile() == false) {
    840             File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML);
    841             if (mainBuildXml.isFile()) {
    842                 try {
    843                     // get the name of the main project and add Test to it.
    844                     String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME,
    845                             new InputSource(new FileInputStream(mainBuildXml)));
    846                     projectName = mainProjectName + "Test";
    847                 } catch (XPathExpressionException e) {
    848                     // it's ok, updateProject() will figure out a name automatically.
    849                     // We do log the error though as the build.xml file may be broken.
    850                     mLog.warning("Failed to parse %1$s.\n" +
    851                             "File may not be valid. Consider running 'android update project' on the main project.",
    852                             mainBuildXml.getPath());
    853                 } catch (FileNotFoundException e) {
    854                     // should not happen since we check first.
    855                 }
    856             }
    857         }
    858 
    859         // now update the project as if it's a normal project
    860         if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) {
    861             // error message has already been displayed.
    862             return;
    863         }
    864 
    865         // add the test project specific properties.
    866         // At this point, we know build.prop has been renamed ant.prop
    867         ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT);
    868         ProjectPropertiesWorkingCopy antWorkingCopy;
    869         if (antProps == null) {
    870             antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT);
    871         } else {
    872             antWorkingCopy = antProps.makeWorkingCopy();
    873         }
    874 
    875         // set or replace the path to the main project
    876         antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject);
    877         try {
    878             antWorkingCopy.save();
    879             println("Updated %1$s", PropertyType.ANT.getFilename());
    880         } catch (Exception e) {
    881             mLog.error(e, "Failed to write %1$s file in '%2$s'",
    882                     PropertyType.ANT.getFilename(),
    883                     folderPath);
    884             return;
    885         }
    886     }
    887 
    888     /**
    889      * Checks whether the give <var>folderPath</var> is a valid project folder, and returns
    890      * a {@link FileWrapper} to the required file.
    891      * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it.
    892      * <p/>Any error are output using {@link #mLog}.
    893      * @param folderPath the folder to check
    894      * @param requiredFilename the file name of the file that's required.
    895      * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise.
    896      */
    897     private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) {
    898         // project folder must exist and be a directory, since this is an update
    899         FolderWrapper projectFolder = new FolderWrapper(folderPath);
    900         if (!projectFolder.isDirectory()) {
    901             mLog.error(null, "Project folder '%1$s' is not a valid directory.",
    902                     projectFolder);
    903             return null;
    904         }
    905 
    906         // Check AndroidManifest.xml is present
    907         FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename);
    908         if (!requireFile.isFile()) {
    909             mLog.error(null,
    910                     "%1$s is not a valid project (%2$s not found).",
    911                     folderPath, requiredFilename);
    912             return null;
    913         }
    914 
    915         return requireFile;
    916     }
    917 
    918     /**
    919      * Looks for a given regex in a file and returns the matcher if any line of the input file
    920      * contains the requested regexp.
    921      *
    922      * @param file the file to search.
    923      * @param regexp the regexp to search for.
    924      *
    925      * @return a Matcher or null if the regexp is not found.
    926      */
    927     private Matcher checkFileContainsRegexp(File file, String regexp) {
    928         Pattern p = Pattern.compile(regexp);
    929 
    930         try {
    931             BufferedReader in = new BufferedReader(new FileReader(file));
    932             String line;
    933 
    934             while ((line = in.readLine()) != null) {
    935                 Matcher m = p.matcher(line);
    936                 if (m.find()) {
    937                     return m;
    938                 }
    939             }
    940 
    941             in.close();
    942         } catch (Exception e) {
    943             // ignore
    944         }
    945 
    946         return null;
    947     }
    948 
    949     /**
    950      * Extracts a "full" package & activity name from an AndroidManifest.xml.
    951      * <p/>
    952      * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
    953      * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}.
    954      * When no activity is found, this key is not created.
    955      *
    956      * @param manifestFile The AndroidManifest.xml file
    957      * @param outKeywords  Place where to put the out parameters: package and activity names.
    958      * @return True if the package/activity was parsed and updated in the keyword dictionary.
    959      */
    960     private boolean extractPackageFromManifest(File manifestFile,
    961             Map<String, String> outKeywords) {
    962         try {
    963             XPath xpath = AndroidXPathFactory.newXPath();
    964 
    965             InputSource source = new InputSource(new FileReader(manifestFile));
    966             String packageName = xpath.evaluate("/manifest/@package", source);
    967 
    968             source = new InputSource(new FileReader(manifestFile));
    969 
    970             // Select the "android:name" attribute of all <activity> nodes but only if they
    971             // contain a sub-node <intent-filter><action> with an "android:name" attribute which
    972             // is 'android.intent.action.MAIN' and an <intent-filter><category> with an
    973             // "android:name" attribute which is 'android.intent.category.LAUNCHER'
    974             String expression = String.format("/manifest/application/activity" +
    975                     "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
    976                     "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
    977                     "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX);
    978 
    979             NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
    980                     XPathConstants.NODESET);
    981 
    982             // If we get here, both XPath expressions were valid so we're most likely dealing
    983             // with an actual AndroidManifest.xml file. The nodes may not have the requested
    984             // attributes though, if which case we should warn.
    985 
    986             if (packageName == null || packageName.length() == 0) {
    987                 mLog.error(null,
    988                         "Missing <manifest package=\"...\"> in '%1$s'",
    989                         manifestFile.getName());
    990                 return false;
    991             }
    992 
    993             // Get the first activity that matched earlier. If there is no activity,
    994             // activityName is set to an empty string and the generated "combined" name
    995             // will be in the form "package." (with a dot at the end).
    996             String activityName = "";
    997             if (activityNames.getLength() > 0) {
    998                 activityName = activityNames.item(0).getNodeValue();
    999             }
   1000 
   1001             if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
   1002                 println("WARNING: There is more than one activity defined in '%1$s'.\n" +
   1003                         "Only the first one will be used. If this is not appropriate, you need\n" +
   1004                         "to specify one of these values manually instead:",
   1005                         manifestFile.getName());
   1006 
   1007                 for (int i = 0; i < activityNames.getLength(); i++) {
   1008                     String name = activityNames.item(i).getNodeValue();
   1009                     name = combinePackageActivityNames(packageName, name);
   1010                     println("- %1$s", name);
   1011                 }
   1012             }
   1013 
   1014             if (activityName.length() == 0) {
   1015                 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
   1016                         "No activity will be generated.",
   1017                         AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName());
   1018             } else {
   1019                 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName);
   1020             }
   1021 
   1022             outKeywords.put(PH_PACKAGE, packageName);
   1023             return true;
   1024 
   1025         } catch (IOException e) {
   1026             mLog.error(e, "Failed to read %1$s", manifestFile.getName());
   1027         } catch (XPathExpressionException e) {
   1028             Throwable t = e.getCause();
   1029             mLog.error(t == null ? e : t,
   1030                     "Failed to parse %1$s",
   1031                     manifestFile.getName());
   1032         }
   1033 
   1034         return false;
   1035     }
   1036 
   1037     private String combinePackageActivityNames(String packageName, String activityName) {
   1038         // Activity Name can have 3 forms:
   1039         // - ".Name" means this is a class name in the given package name.
   1040         //    The full FQCN is thus packageName + ".Name"
   1041         // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
   1042         // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
   1043         //   To be valid, the package name should have at least two components. This is checked
   1044         //   later during the creation of the build.xml file, so we just need to detect there's
   1045         //   a dot but not at pos==0.
   1046 
   1047         int pos = activityName.indexOf('.');
   1048         if (pos == 0) {
   1049             return packageName + activityName;
   1050         } else if (pos > 0) {
   1051             return activityName;
   1052         } else {
   1053             return packageName + "." + activityName;
   1054         }
   1055     }
   1056 
   1057     /**
   1058      * Installs a new file that is based on a template file provided by a given target.
   1059      * Each match of each key from the place-holder map in the template will be replaced with its
   1060      * corresponding value in the created file.
   1061      *
   1062      * @param templateName the name of to the template file
   1063      * @param destFile the path to the destination file, relative to the project
   1064      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
   1065      * @param target the Target of the project that will be providing the template.
   1066      * @throws ProjectCreateException
   1067      */
   1068     private void installTargetTemplate(String templateName, File destFile,
   1069             Map<String, String> placeholderMap, IAndroidTarget target)
   1070             throws ProjectCreateException {
   1071         // query the target for its template directory
   1072         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
   1073         final String sourcePath = templateFolder + File.separator + templateName;
   1074 
   1075         installFullPathTemplate(sourcePath, destFile, placeholderMap);
   1076     }
   1077 
   1078     /**
   1079      * Installs a new file that is based on a template file provided by the tools folder.
   1080      * Each match of each key from the place-holder map in the template will be replaced with its
   1081      * corresponding value in the created file.
   1082      *
   1083      * @param templateName the name of to the template file
   1084      * @param destFile the path to the destination file, relative to the project
   1085      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
   1086      * @throws ProjectCreateException
   1087      */
   1088     private void installTemplate(String templateName, File destFile,
   1089             Map<String, String> placeholderMap)
   1090             throws ProjectCreateException {
   1091         // query the target for its template directory
   1092         String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
   1093         final String sourcePath = templateFolder + File.separator + templateName;
   1094 
   1095         installFullPathTemplate(sourcePath, destFile, placeholderMap);
   1096     }
   1097 
   1098     /**
   1099      * Installs a new file that is based on a template.
   1100      * Each match of each key from the place-holder map in the template will be replaced with its
   1101      * corresponding value in the created file.
   1102      *
   1103      * @param sourcePath the full path to the source template file
   1104      * @param destFile the destination file
   1105      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
   1106      * @throws ProjectCreateException
   1107      */
   1108     private void installFullPathTemplate(String sourcePath, File destFile,
   1109             Map<String, String> placeholderMap) throws ProjectCreateException {
   1110 
   1111         boolean existed = destFile.exists();
   1112 
   1113         try {
   1114             BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
   1115             BufferedReader in = new BufferedReader(new FileReader(sourcePath));
   1116             String line;
   1117 
   1118             while ((line = in.readLine()) != null) {
   1119                 if (placeholderMap != null) {
   1120                     for (Map.Entry<String, String> entry : placeholderMap.entrySet()) {
   1121                         line = line.replace(entry.getKey(), entry.getValue());
   1122                     }
   1123                 }
   1124 
   1125                 out.write(line);
   1126                 out.newLine();
   1127             }
   1128 
   1129             out.close();
   1130             in.close();
   1131         } catch (Exception e) {
   1132             throw new ProjectCreateException(e, "Could not access %1$s: %2$s",
   1133                     destFile, e.getMessage());
   1134         }
   1135 
   1136         println("%1$s file %2$s",
   1137                 existed ? "Updated" : "Added",
   1138                 destFile);
   1139     }
   1140 
   1141     /**
   1142      * Installs the project icons.
   1143      * @param resourceFolder the resource folder
   1144      * @param target the target of the project.
   1145      * @return true if any icon was installed.
   1146      */
   1147     private boolean installIcons(File resourceFolder, IAndroidTarget target)
   1148             throws ProjectCreateException {
   1149         // query the target for its template directory
   1150         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
   1151 
   1152         boolean installedIcon = false;
   1153 
   1154         installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder,
   1155                 "drawable-hdpi");
   1156         installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder,
   1157                 "drawable-mdpi");
   1158         installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder,
   1159                 "drawable-ldpi");
   1160 
   1161         return installedIcon;
   1162     }
   1163 
   1164     /**
   1165      * Installs an Icon in the project.
   1166      * @return true if the icon was installed.
   1167      */
   1168     private boolean installIcon(String templateFolder, String iconName, File resourceFolder,
   1169             String folderName) throws ProjectCreateException {
   1170         File icon = new File(templateFolder, iconName);
   1171         if (icon.exists()) {
   1172             File drawable = createDirs(resourceFolder, folderName);
   1173             installBinaryFile(icon, new File(drawable, "ic_launcher.png"));
   1174             return true;
   1175         }
   1176 
   1177         return false;
   1178     }
   1179 
   1180     /**
   1181      * Installs a binary file
   1182      * @param source the source file to copy
   1183      * @param destination the destination file to write
   1184      * @throws ProjectCreateException
   1185      */
   1186     private void installBinaryFile(File source, File destination) throws ProjectCreateException {
   1187         byte[] buffer = new byte[8192];
   1188 
   1189         FileInputStream fis = null;
   1190         FileOutputStream fos = null;
   1191         try {
   1192             fis = new FileInputStream(source);
   1193             fos = new FileOutputStream(destination);
   1194 
   1195             int read;
   1196             while ((read = fis.read(buffer)) != -1) {
   1197                 fos.write(buffer, 0, read);
   1198             }
   1199 
   1200         } catch (FileNotFoundException e) {
   1201             // shouldn't happen since we check before.
   1202         } catch (IOException e) {
   1203             throw new ProjectCreateException(e, "Failed to read binary file: %1$s",
   1204                     source.getAbsolutePath());
   1205         } finally {
   1206             if (fis != null) {
   1207                 try {
   1208                     fis.close();
   1209                 } catch (IOException e) {
   1210                     // ignore
   1211                 }
   1212             }
   1213             if (fos != null) {
   1214                 try {
   1215                     fos.close();
   1216                 } catch (IOException e) {
   1217                     // ignore
   1218                 }
   1219             }
   1220         }
   1221 
   1222     }
   1223 
   1224     /**
   1225      * Prints a message unless silence is enabled.
   1226      * <p/>
   1227      * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
   1228      * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
   1229      *
   1230      * @param format Format for String.format
   1231      * @param args Arguments for String.format
   1232      */
   1233     private void println(String format, Object... args) {
   1234         if (mLevel != OutputLevel.SILENT) {
   1235             if (!format.endsWith("\n")) {
   1236                 format += "\n";
   1237             }
   1238             mLog.printf(format, args);
   1239         }
   1240     }
   1241 
   1242     /**
   1243      * Creates a new folder, along with any parent folders that do not exists.
   1244      *
   1245      * @param parent the parent folder
   1246      * @param name the name of the directory to create.
   1247      * @throws ProjectCreateException
   1248      */
   1249     private File createDirs(File parent, String name) throws ProjectCreateException {
   1250         final File newFolder = new File(parent, name);
   1251         boolean existedBefore = true;
   1252 
   1253         if (!newFolder.exists()) {
   1254             if (!newFolder.mkdirs()) {
   1255                 throw new ProjectCreateException("Could not create directory: %1$s", newFolder);
   1256             }
   1257             existedBefore = false;
   1258         }
   1259 
   1260         if (newFolder.isDirectory()) {
   1261             if (!newFolder.canWrite()) {
   1262                 throw new ProjectCreateException("Path is not writable: %1$s", newFolder);
   1263             }
   1264         } else {
   1265             throw new ProjectCreateException("Path is not a directory: %1$s", newFolder);
   1266         }
   1267 
   1268         if (!existedBefore) {
   1269             try {
   1270                 println("Created directory %1$s", newFolder.getCanonicalPath());
   1271             } catch (IOException e) {
   1272                 throw new ProjectCreateException(
   1273                         "Could not determine canonical path of created directory", e);
   1274             }
   1275         }
   1276 
   1277         return newFolder;
   1278     }
   1279 
   1280     /**
   1281      * Strips the string of beginning and trailing characters (multiple
   1282      * characters will be stripped, example stripString("..test...", '.')
   1283      * results in "test";
   1284      *
   1285      * @param s the string to strip
   1286      * @param strip the character to strip from beginning and end
   1287      * @return the stripped string or the empty string if everything is stripped.
   1288      */
   1289     private static String stripString(String s, char strip) {
   1290         final int sLen = s.length();
   1291         int newStart = 0, newEnd = sLen - 1;
   1292 
   1293         while (newStart < sLen && s.charAt(newStart) == strip) {
   1294           newStart++;
   1295         }
   1296         while (newEnd >= 0 && s.charAt(newEnd) == strip) {
   1297           newEnd--;
   1298         }
   1299 
   1300         /*
   1301          * newEnd contains a char we want, and substring takes end as being
   1302          * exclusive
   1303          */
   1304         newEnd++;
   1305 
   1306         if (newStart >= sLen || newEnd < 0) {
   1307             return "";
   1308         }
   1309 
   1310         return s.substring(newStart, newEnd);
   1311     }
   1312 }
   1313