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 = new File(folderPath);
    179         if (!projectFolder.exists()) {
    180 
    181             boolean created = false;
    182             Throwable t = null;
    183             try {
    184                 created = projectFolder.mkdirs();
    185             } catch (Exception e) {
    186                 t = e;
    187             }
    188 
    189             if (created) {
    190                 println("Created project directory: %1$s", projectFolder);
    191             } else {
    192                 mLog.error(t, "Could not create directory: %1$s", projectFolder);
    193                 return;
    194             }
    195         } else {
    196             Exception e = null;
    197             String error = null;
    198             try {
    199                 String[] content = projectFolder.list();
    200                 if (content == null) {
    201                     error = "Project folder '%1$s' is not a directory.";
    202                 } else if (content.length != 0) {
    203                     error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead.";
    204                 }
    205             } catch (Exception e1) {
    206                 e = e1;
    207             }
    208 
    209             if (e != null || error != null) {
    210                 mLog.error(e, error, projectFolder, SdkConstants.androidCmdName());
    211             }
    212         }
    213 
    214         try {
    215             boolean isTestProject = pathToMainProject != null;
    216 
    217             // first create the project properties.
    218 
    219             // location of the SDK goes in localProperty
    220             ProjectProperties localProperties = ProjectProperties.create(folderPath,
    221                     PropertyType.LOCAL);
    222             localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
    223             localProperties.save();
    224 
    225             // target goes in default properties
    226             ProjectProperties defaultProperties = ProjectProperties.create(folderPath,
    227                     PropertyType.DEFAULT);
    228             defaultProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
    229             if (library) {
    230                 defaultProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true");
    231             }
    232             defaultProperties.save();
    233 
    234             // create a build.properties file with just the application package
    235             ProjectProperties buildProperties = ProjectProperties.create(folderPath,
    236                     PropertyType.BUILD);
    237 
    238             // only put application.package for older target where the rules file didn't.
    239             // grab it through xpath
    240             if (target.getVersion().getApiLevel() < 4) {
    241                 buildProperties.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE, packageName);
    242             }
    243 
    244             if (isTestProject) {
    245                 buildProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT,
    246                         pathToMainProject);
    247             }
    248 
    249             buildProperties.save();
    250 
    251             // create the map for place-holders of values to replace in the templates
    252             final HashMap<String, String> keywords = new HashMap<String, String>();
    253 
    254             // create the required folders.
    255             // compute src folder path
    256             final String packagePath =
    257                 stripString(packageName.replace(".", File.separator),
    258                         File.separatorChar);
    259 
    260             // put this path in the place-holder map for project files that needs to list
    261             // files manually.
    262             keywords.put(PH_JAVA_FOLDER, packagePath);
    263             keywords.put(PH_PACKAGE, packageName);
    264 
    265 
    266             // compute some activity related information
    267             String fqActivityName = null, activityPath = null, activityClassName = null;
    268             String originalActivityEntry = activityEntry;
    269             String originalActivityClassName = null;
    270             if (activityEntry != null) {
    271                 if (isTestProject) {
    272                     // append Test so that it doesn't collide with the main project activity.
    273                     activityEntry += "Test";
    274 
    275                     // get the classname from the original activity entry.
    276                     int pos = originalActivityEntry.lastIndexOf('.');
    277                     if (pos != -1) {
    278                         originalActivityClassName = originalActivityEntry.substring(pos + 1);
    279                     } else {
    280                         originalActivityClassName = originalActivityEntry;
    281                     }
    282                 }
    283 
    284                 // get the fully qualified name of the activity
    285                 fqActivityName = AndroidManifest.combinePackageAndClassName(packageName,
    286                         activityEntry);
    287 
    288                 // get the activity path (replace the . to /)
    289                 activityPath = stripString(fqActivityName.replace(".", File.separator),
    290                         File.separatorChar);
    291 
    292                 // remove the last segment, so that we only have the path to the activity, but
    293                 // not the activity filename itself.
    294                 activityPath = activityPath.substring(0,
    295                         activityPath.lastIndexOf(File.separatorChar));
    296 
    297                 // finally, get the class name for the activity
    298                 activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1);
    299             }
    300 
    301             // at this point we have the following for the activity:
    302             // activityEntry: this is the manifest entry. For instance .MyActivity
    303             // fqActivityName: full-qualified class name: com.foo.MyActivity
    304             // activityClassName: only the classname: MyActivity
    305             // originalActivityClassName: the classname of the activity being tested (if applicable)
    306 
    307             // Add whatever activity info is needed in the place-holder map.
    308             // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests).
    309             if (target.getVersion().getApiLevel() < 4) { // legacy
    310                 if (originalActivityEntry != null) {
    311                     keywords.put(PH_ACTIVITY_NAME, originalActivityEntry);
    312                 }
    313             } else {
    314                 // newer templates make a difference between the manifest entries, classnames,
    315                 // as well as the main and test classes.
    316                 if (activityEntry != null) {
    317                     keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry);
    318                     keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName);
    319                     keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName);
    320                     if (originalActivityClassName != null) {
    321                         keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName);
    322                     }
    323                 }
    324             }
    325 
    326             // Take the project name from the command line if there's one
    327             if (projectName != null) {
    328                 keywords.put(PH_PROJECT_NAME, projectName);
    329             } else {
    330                 if (activityClassName != null) {
    331                     // Use the activity class name as project name
    332                     keywords.put(PH_PROJECT_NAME, activityClassName);
    333                 } else {
    334                     // We need a project name. Just pick up the basename of the project
    335                     // directory.
    336                     projectName = projectFolder.getName();
    337                     keywords.put(PH_PROJECT_NAME, projectName);
    338                 }
    339             }
    340 
    341             // create the source folder for the activity
    342             if (activityClassName != null) {
    343                 String srcActivityFolderPath =
    344                         SdkConstants.FD_SOURCES + File.separator + activityPath;
    345                 File sourceFolder = createDirs(projectFolder, srcActivityFolderPath);
    346 
    347                 String javaTemplate = isTestProject ? "java_tests_file.template"
    348                         : "java_file.template";
    349                 String activityFileName = activityClassName + ".java";
    350 
    351                 installTemplate(javaTemplate, new File(sourceFolder, activityFileName),
    352                         keywords, target);
    353             } else {
    354                 // we should at least create 'src'
    355                 createDirs(projectFolder, SdkConstants.FD_SOURCES);
    356             }
    357 
    358             // create other useful folders
    359             File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES);
    360             createDirs(projectFolder, SdkConstants.FD_OUTPUT);
    361             createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS);
    362 
    363             if (isTestProject == false) {
    364                 /* Make res files only for non test projects */
    365                 File valueFolder = createDirs(resourceFolder, SdkConstants.FD_VALUES);
    366                 installTemplate("strings.template", new File(valueFolder, "strings.xml"),
    367                         keywords, target);
    368 
    369                 File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_LAYOUT);
    370                 installTemplate("layout.template", new File(layoutFolder, "main.xml"),
    371                         keywords, target);
    372 
    373                 // create the icons
    374                 if (installIcons(resourceFolder, target)) {
    375                     keywords.put(PH_ICON, "android:icon=\"@drawable/icon\"");
    376                 } else {
    377                     keywords.put(PH_ICON, "");
    378                 }
    379             }
    380 
    381             /* Make AndroidManifest.xml and build.xml files */
    382             String manifestTemplate = "AndroidManifest.template";
    383             if (isTestProject) {
    384                 manifestTemplate = "AndroidManifest.tests.template";
    385             }
    386 
    387             installTemplate(manifestTemplate,
    388                     new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML),
    389                     keywords, target);
    390 
    391             installTemplate("build.template",
    392                     new File(projectFolder, SdkConstants.FN_BUILD_XML),
    393                     keywords);
    394         } catch (ProjectCreateException e) {
    395             mLog.error(e, null);
    396         } catch (IOException e) {
    397             mLog.error(e, null);
    398         }
    399     }
    400 
    401     /**
    402      * Updates an existing project.
    403      * <p/>
    404      * Workflow:
    405      * <ul>
    406      * <li> Check AndroidManifest.xml is present (required)
    407      * <li> Check there's a default.properties with a target *or* --target was specified
    408      * <li> Update default.prop if --target was specified
    409      * <li> Refresh/create "sdk" in local.properties
    410      * <li> Build.xml: create if not present or no <androidinit(\w|/>) in it
    411      * </ul>
    412      *
    413      * @param folderPath the folder of the project to update. This folder must exist.
    414      * @param target the project target. Can be null.
    415      * @param projectName The project name from --name. Can be null.
    416      * @param libraryPath the path to a library to add to the references. Can be null.
    417      * @return true if the project was successfully updated.
    418      */
    419     public boolean updateProject(String folderPath, IAndroidTarget target, String projectName,
    420             String libraryPath) {
    421         // since this is an update, check the folder does point to a project
    422         File androidManifest = checkProjectFolder(folderPath);
    423         if (androidManifest == null) {
    424             return false;
    425         }
    426 
    427         // get the parent File.
    428         File projectFolder = androidManifest.getParentFile();
    429 
    430         // Check there's a default.properties with a target *or* --target was specified
    431         IAndroidTarget originalTarget = null;
    432         ProjectProperties props = ProjectProperties.load(folderPath, PropertyType.DEFAULT);
    433         if (props != null) {
    434             String targetHash = props.getProperty(ProjectProperties.PROPERTY_TARGET);
    435             originalTarget = mSdkManager.getTargetFromHashString(targetHash);
    436         }
    437 
    438         if (originalTarget == null && target == null) {
    439             mLog.error(null,
    440                 "The project either has no target set or the target is invalid.\n" +
    441                 "Please provide a --target to the '%1$s update' command.",
    442                 SdkConstants.androidCmdName());
    443             return false;
    444         }
    445 
    446         // before doing anything, make sure library (if present) can be applied.
    447         if (libraryPath != null) {
    448             IAndroidTarget finalTarget = target != null ? target : originalTarget;
    449             if (finalTarget.getProperty(SdkConstants.PROP_SDK_SUPPORT_LIBRARY, false) == false) {
    450                 mLog.error(null,
    451                         "The build system for this project target (%1$s) does not support libraries",
    452                         finalTarget.getFullName());
    453                 return false;
    454             }
    455         }
    456 
    457         boolean saveDefaultProps = false;
    458 
    459         // Update default.prop if --target was specified
    460         if (target != null) {
    461             // we already attempted to load the file earlier, if that failed, create it.
    462             if (props == null) {
    463                 props = ProjectProperties.create(folderPath, PropertyType.DEFAULT);
    464             }
    465 
    466             // set or replace the target
    467             props.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
    468             saveDefaultProps = true;
    469         }
    470 
    471         if (libraryPath != null) {
    472             // at this point, the default properties already exists, either because they were
    473             // already there or because they were created with a new target
    474 
    475             // check the reference is valid
    476             File libProject = new File(libraryPath);
    477             String resolvedPath;
    478             if (libProject.isAbsolute() == false) {
    479                 libProject = new File(folderPath, libraryPath);
    480                 try {
    481                     resolvedPath = libProject.getCanonicalPath();
    482                 } catch (IOException e) {
    483                     mLog.error(e, "Unable to resolve path to library project: %1$s", libraryPath);
    484                     return false;
    485                 }
    486             } else {
    487                 resolvedPath = libProject.getAbsolutePath();
    488             }
    489 
    490             println("Resolved location of library project to: %1$s", resolvedPath);
    491 
    492             // check the lib project exists
    493             if (checkProjectFolder(resolvedPath) == null) {
    494                 mLog.error(null, "No Android Manifest at: %1$s", resolvedPath);
    495                 return false;
    496             }
    497 
    498             // look for other references to figure out the index
    499             int index = 1;
    500             while (true) {
    501                 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index);
    502                 String ref = props.getProperty(propName);
    503                 if (ref == null) {
    504                     break;
    505                 } else {
    506                     index++;
    507                 }
    508             }
    509 
    510             String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index);
    511             props.setProperty(propName, libraryPath);
    512             saveDefaultProps = true;
    513         }
    514 
    515         // save the default props if needed.
    516         if (saveDefaultProps) {
    517             try {
    518                 props.save();
    519                 println("Updated %1$s", PropertyType.DEFAULT.getFilename());
    520             } catch (IOException e) {
    521                 mLog.error(e, "Failed to write %1$s file in '%2$s'",
    522                         PropertyType.DEFAULT.getFilename(),
    523                         folderPath);
    524                 return false;
    525             }
    526         }
    527 
    528         // Refresh/create "sdk" in local.properties
    529         // because the file may already exists and contain other values (like apk config),
    530         // we first try to load it.
    531         props = ProjectProperties.load(folderPath, PropertyType.LOCAL);
    532         if (props == null) {
    533             props = ProjectProperties.create(folderPath, PropertyType.LOCAL);
    534         }
    535 
    536         // set or replace the sdk location.
    537         props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
    538         try {
    539             props.save();
    540             println("Updated %1$s", PropertyType.LOCAL.getFilename());
    541         } catch (IOException e) {
    542             mLog.error(e, "Failed to write %1$s file in '%2$s'",
    543                     PropertyType.LOCAL.getFilename(),
    544                     folderPath);
    545             return false;
    546         }
    547 
    548         // Build.xml: create if not present or no <androidinit/> in it
    549         File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML);
    550         boolean needsBuildXml = projectName != null || !buildXml.exists();
    551         if (!needsBuildXml) {
    552             // Look for for a classname="com.android.ant.SetupTask" attribute
    553             needsBuildXml = !checkFileContainsRegexp(buildXml,
    554                     "classname=\"com.android.ant.SetupTask\"");  //$NON-NLS-1$
    555         }
    556         if (!needsBuildXml) {
    557             // Note that "<setup" must be followed by either a whitespace, a "/" (for the
    558             // XML /> closing tag) or an end-of-line. This way we know the XML tag is really this
    559             // one and later we will be able to use an "androidinit2" tag or such as necessary.
    560             needsBuildXml = !checkFileContainsRegexp(buildXml, "<setup(?:\\s|/|$)");  //$NON-NLS-1$
    561         }
    562         if (needsBuildXml) {
    563             if (buildXml.exists()) {
    564                 println("File %1$s is too old and needs to be updated.", SdkConstants.FN_BUILD_XML);
    565             }
    566         }
    567 
    568         if (needsBuildXml) {
    569             // create the map for place-holders of values to replace in the templates
    570             final HashMap<String, String> keywords = new HashMap<String, String>();
    571 
    572             // Take the project name from the command line if there's one
    573             if (projectName != null) {
    574                 keywords.put(PH_PROJECT_NAME, projectName);
    575             } else {
    576                 extractPackageFromManifest(androidManifest, keywords);
    577                 if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) {
    578                     String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME);
    579                     // keep only the last segment if applicable
    580                     int pos = activity.lastIndexOf('.');
    581                     if (pos != -1) {
    582                         activity = activity.substring(pos + 1);
    583                     }
    584 
    585                     // Use the activity as project name
    586                     keywords.put(PH_PROJECT_NAME, activity);
    587                 } else {
    588                     // We need a project name. Just pick up the basename of the project
    589                     // directory.
    590                     projectName = projectFolder.getName();
    591                     keywords.put(PH_PROJECT_NAME, projectName);
    592                 }
    593             }
    594 
    595             if (mLevel == OutputLevel.VERBOSE) {
    596                 println("Regenerating %1$s with project name %2$s",
    597                         SdkConstants.FN_BUILD_XML,
    598                         keywords.get(PH_PROJECT_NAME));
    599             }
    600 
    601             try {
    602                 installTemplate("build.template",
    603                         new File(projectFolder, SdkConstants.FN_BUILD_XML),
    604                         keywords);
    605             } catch (ProjectCreateException e) {
    606                 mLog.error(e, null);
    607                 return false;
    608             }
    609         }
    610 
    611         return true;
    612     }
    613 
    614     /**
    615      * Updates a test project with a new path to the main (tested) project.
    616      * @param folderPath the path of the test project.
    617      * @param pathToMainProject the path to the main project, relative to the test project.
    618      */
    619     public void updateTestProject(final String folderPath, final String pathToMainProject,
    620             final SdkManager sdkManager) {
    621         // since this is an update, check the folder does point to a project
    622         if (checkProjectFolder(folderPath) == null) {
    623             return;
    624         }
    625 
    626         // check the path to the main project is valid.
    627         File mainProject = new File(pathToMainProject);
    628         String resolvedPath;
    629         if (mainProject.isAbsolute() == false) {
    630             mainProject = new File(folderPath, pathToMainProject);
    631             try {
    632                 resolvedPath = mainProject.getCanonicalPath();
    633             } catch (IOException e) {
    634                 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject);
    635                 return;
    636             }
    637         } else {
    638             resolvedPath = mainProject.getAbsolutePath();
    639         }
    640 
    641         println("Resolved location of main project to: %1$s", resolvedPath);
    642 
    643         // check the main project exists
    644         if (checkProjectFolder(resolvedPath) == null) {
    645             mLog.error(null, "No Android Manifest at: %1$s", resolvedPath);
    646             return;
    647         }
    648 
    649         // now get the target from the main project
    650         ProjectProperties defaultProp = ProjectProperties.load(resolvedPath, PropertyType.DEFAULT);
    651         if (defaultProp == null) {
    652             mLog.error(null, "No %1$s at: %2$s", PropertyType.DEFAULT.getFilename(), resolvedPath);
    653             return;
    654         }
    655 
    656         String targetHash = defaultProp.getProperty(ProjectProperties.PROPERTY_TARGET);
    657         if (targetHash == null) {
    658             mLog.error(null, "%1$s in the main project has no target property.",
    659                     PropertyType.DEFAULT.getFilename());
    660             return;
    661         }
    662 
    663         IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash);
    664         if (target == null) {
    665             mLog.error(null, "Main project target %1$s is not a valid target.", targetHash);
    666             return;
    667         }
    668 
    669         // look for the name of the project. If build.xml does not exist,
    670         // query the main project build.xml for its name
    671         String projectName = null;
    672         XPathFactory factory = XPathFactory.newInstance();
    673         XPath xpath = factory.newXPath();
    674 
    675         File testBuildXml = new File(folderPath, "build.xml");
    676         if (testBuildXml.isFile()) {
    677             try {
    678                 projectName = xpath.evaluate("/project/@name",
    679                         new InputSource(new FileInputStream(testBuildXml)));
    680             } catch (XPathExpressionException e) {
    681                 // looks like the build.xml is wrong, we'll create a new one, and get its name
    682                 // from the parent.
    683             } catch (FileNotFoundException e) {
    684                 // looks like the build.xml is wrong, we'll create a new one, and get its name
    685                 // from the parent.
    686             }
    687         }
    688 
    689         // if the project name is still unknown, get it from the parent.
    690         if (projectName == null) {
    691             try {
    692                 String mainProjectName = xpath.evaluate("/project/@name",
    693                         new InputSource(new FileInputStream(new File(resolvedPath, "build.xml"))));
    694                 projectName = mainProjectName + "Test";
    695             } catch (XPathExpressionException e) {
    696                 mLog.error(e, "Unable to query main project name.");
    697                 return;
    698             } catch (FileNotFoundException e) {
    699                 mLog.error(e, "Unable to query main project name.");
    700                 return;
    701             }
    702         }
    703 
    704         // now update the project as if it's a normal project
    705         if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) {
    706             // error message has already been displayed.
    707             return;
    708         }
    709 
    710         // add the test project specific properties.
    711         ProjectProperties buildProps = ProjectProperties.load(folderPath, PropertyType.BUILD);
    712         if (buildProps == null) {
    713             buildProps = ProjectProperties.create(folderPath, PropertyType.BUILD);
    714         }
    715 
    716         // set or replace the path to the main project
    717         buildProps.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject);
    718         try {
    719             buildProps.save();
    720             println("Updated %1$s", PropertyType.BUILD.getFilename());
    721         } catch (IOException e) {
    722             mLog.error(e, "Failed to write %1$s file in '%2$s'",
    723                     PropertyType.BUILD.getFilename(),
    724                     folderPath);
    725             return;
    726         }
    727 
    728     }
    729 
    730     /**
    731      * Checks whether the give <var>folderPath</var> is a valid project folder, and returns
    732      * a {@link File} to the AndroidManifest.xml file.
    733      * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it.
    734      * <p/>Any error are output using {@link #mLog}.
    735      * @param folderPath the folder to check
    736      * @return a {@link File} to the AndroidManifest.xml file, or null otherwise.
    737      */
    738     private File checkProjectFolder(String folderPath) {
    739         // project folder must exist and be a directory, since this is an update
    740         File projectFolder = new File(folderPath);
    741         if (!projectFolder.isDirectory()) {
    742             mLog.error(null, "Project folder '%1$s' is not a valid directory, this is not an Android project you can update.",
    743                     projectFolder);
    744             return null;
    745         }
    746 
    747         // Check AndroidManifest.xml is present
    748         File androidManifest = new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML);
    749         if (!androidManifest.isFile()) {
    750             mLog.error(null,
    751                     "%1$s not found in '%2$s', this is not an Android project you can update.",
    752                     SdkConstants.FN_ANDROID_MANIFEST_XML,
    753                     folderPath);
    754             return null;
    755         }
    756 
    757         return androidManifest;
    758     }
    759 
    760     /**
    761      * Returns true if any line of the input file contains the requested regexp.
    762      */
    763     private boolean checkFileContainsRegexp(File file, String regexp) {
    764         Pattern p = Pattern.compile(regexp);
    765 
    766         try {
    767             BufferedReader in = new BufferedReader(new FileReader(file));
    768             String line;
    769 
    770             while ((line = in.readLine()) != null) {
    771                 if (p.matcher(line).find()) {
    772                     return true;
    773                 }
    774             }
    775 
    776             in.close();
    777         } catch (Exception e) {
    778             // ignore
    779         }
    780 
    781         return false;
    782     }
    783 
    784     /**
    785      * Extracts a "full" package & activity name from an AndroidManifest.xml.
    786      * <p/>
    787      * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
    788      * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}.
    789      * When no activity is found, this key is not created.
    790      *
    791      * @param manifestFile The AndroidManifest.xml file
    792      * @param outKeywords  Place where to put the out parameters: package and activity names.
    793      * @return True if the package/activity was parsed and updated in the keyword dictionary.
    794      */
    795     private boolean extractPackageFromManifest(File manifestFile,
    796             Map<String, String> outKeywords) {
    797         try {
    798             XPath xpath = AndroidXPathFactory.newXPath();
    799 
    800             InputSource source = new InputSource(new FileReader(manifestFile));
    801             String packageName = xpath.evaluate("/manifest/@package", source);
    802 
    803             source = new InputSource(new FileReader(manifestFile));
    804 
    805             // Select the "android:name" attribute of all <activity> nodes but only if they
    806             // contain a sub-node <intent-filter><action> with an "android:name" attribute which
    807             // is 'android.intent.action.MAIN' and an <intent-filter><category> with an
    808             // "android:name" attribute which is 'android.intent.category.LAUNCHER'
    809             String expression = String.format("/manifest/application/activity" +
    810                     "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
    811                     "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
    812                     "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX);
    813 
    814             NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
    815                     XPathConstants.NODESET);
    816 
    817             // If we get here, both XPath expressions were valid so we're most likely dealing
    818             // with an actual AndroidManifest.xml file. The nodes may not have the requested
    819             // attributes though, if which case we should warn.
    820 
    821             if (packageName == null || packageName.length() == 0) {
    822                 mLog.error(null,
    823                         "Missing <manifest package=\"...\"> in '%1$s'",
    824                         manifestFile.getName());
    825                 return false;
    826             }
    827 
    828             // Get the first activity that matched earlier. If there is no activity,
    829             // activityName is set to an empty string and the generated "combined" name
    830             // will be in the form "package." (with a dot at the end).
    831             String activityName = "";
    832             if (activityNames.getLength() > 0) {
    833                 activityName = activityNames.item(0).getNodeValue();
    834             }
    835 
    836             if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
    837                 println("WARNING: There is more than one activity defined in '%1$s'.\n" +
    838                         "Only the first one will be used. If this is not appropriate, you need\n" +
    839                         "to specify one of these values manually instead:",
    840                         manifestFile.getName());
    841 
    842                 for (int i = 0; i < activityNames.getLength(); i++) {
    843                     String name = activityNames.item(i).getNodeValue();
    844                     name = combinePackageActivityNames(packageName, name);
    845                     println("- %1$s", name);
    846                 }
    847             }
    848 
    849             if (activityName.length() == 0) {
    850                 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
    851                         "No activity will be generated.",
    852                         AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName());
    853             } else {
    854                 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName);
    855             }
    856 
    857             outKeywords.put(PH_PACKAGE, packageName);
    858             return true;
    859 
    860         } catch (IOException e) {
    861             mLog.error(e, "Failed to read %1$s", manifestFile.getName());
    862         } catch (XPathExpressionException e) {
    863             Throwable t = e.getCause();
    864             mLog.error(t == null ? e : t,
    865                     "Failed to parse %1$s",
    866                     manifestFile.getName());
    867         }
    868 
    869         return false;
    870     }
    871 
    872     private String combinePackageActivityNames(String packageName, String activityName) {
    873         // Activity Name can have 3 forms:
    874         // - ".Name" means this is a class name in the given package name.
    875         //    The full FQCN is thus packageName + ".Name"
    876         // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
    877         // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
    878         //   To be valid, the package name should have at least two components. This is checked
    879         //   later during the creation of the build.xml file, so we just need to detect there's
    880         //   a dot but not at pos==0.
    881 
    882         int pos = activityName.indexOf('.');
    883         if (pos == 0) {
    884             return packageName + activityName;
    885         } else if (pos > 0) {
    886             return activityName;
    887         } else {
    888             return packageName + "." + activityName;
    889         }
    890     }
    891 
    892     /**
    893      * Installs a new file that is based on a template file provided by a given target.
    894      * Each match of each key from the place-holder map in the template will be replaced with its
    895      * corresponding value in the created file.
    896      *
    897      * @param templateName the name of to the template file
    898      * @param destFile the path to the destination file, relative to the project
    899      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
    900      * @param target the Target of the project that will be providing the template.
    901      * @throws ProjectCreateException
    902      */
    903     private void installTemplate(String templateName, File destFile,
    904             Map<String, String> placeholderMap, IAndroidTarget target)
    905             throws ProjectCreateException {
    906         // query the target for its template directory
    907         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
    908         final String sourcePath = templateFolder + File.separator + templateName;
    909 
    910         installFullPathTemplate(sourcePath, destFile, placeholderMap);
    911     }
    912 
    913     /**
    914      * Installs a new file that is based on a template file provided by the tools folder.
    915      * Each match of each key from the place-holder map in the template will be replaced with its
    916      * corresponding value in the created file.
    917      *
    918      * @param templateName the name of to the template file
    919      * @param destFile the path to the destination file, relative to the project
    920      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
    921      * @throws ProjectCreateException
    922      */
    923     private void installTemplate(String templateName, File destFile,
    924             Map<String, String> placeholderMap)
    925             throws ProjectCreateException {
    926         // query the target for its template directory
    927         String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
    928         final String sourcePath = templateFolder + File.separator + templateName;
    929 
    930         installFullPathTemplate(sourcePath, destFile, placeholderMap);
    931     }
    932 
    933     /**
    934      * Installs a new file that is based on a template.
    935      * Each match of each key from the place-holder map in the template will be replaced with its
    936      * corresponding value in the created file.
    937      *
    938      * @param sourcePath the full path to the source template file
    939      * @param destFile the destination file
    940      * @param placeholderMap a map of (place-holder, value) to create the file from the template.
    941      * @throws ProjectCreateException
    942      */
    943     private void installFullPathTemplate(String sourcePath, File destFile,
    944             Map<String, String> placeholderMap) throws ProjectCreateException {
    945 
    946         boolean existed = destFile.exists();
    947 
    948         try {
    949             BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
    950             BufferedReader in = new BufferedReader(new FileReader(sourcePath));
    951             String line;
    952 
    953             while ((line = in.readLine()) != null) {
    954                 if (placeholderMap != null) {
    955                     for (String key : placeholderMap.keySet()) {
    956                         line = line.replace(key, placeholderMap.get(key));
    957                     }
    958                 }
    959 
    960                 out.write(line);
    961                 out.newLine();
    962             }
    963 
    964             out.close();
    965             in.close();
    966         } catch (Exception e) {
    967             throw new ProjectCreateException(e, "Could not access %1$s: %2$s",
    968                     destFile, e.getMessage());
    969         }
    970 
    971         println("%1$s file %2$s",
    972                 existed ? "Updated" : "Added",
    973                 destFile);
    974     }
    975 
    976     /**
    977      * Installs the project icons.
    978      * @param resourceFolder the resource folder
    979      * @param target the target of the project.
    980      * @return true if any icon was installed.
    981      */
    982     private boolean installIcons(File resourceFolder, IAndroidTarget target)
    983             throws ProjectCreateException {
    984         // query the target for its template directory
    985         String templateFolder = target.getPath(IAndroidTarget.TEMPLATES);
    986 
    987         boolean installedIcon = false;
    988 
    989         installedIcon |= installIcon(templateFolder, "icon_hdpi.png", resourceFolder, "drawable-hdpi");
    990         installedIcon |= installIcon(templateFolder, "icon_mdpi.png", resourceFolder, "drawable-mdpi");
    991         installedIcon |= installIcon(templateFolder, "icon_ldpi.png", resourceFolder, "drawable-ldpi");
    992 
    993         return installedIcon;
    994     }
    995 
    996     /**
    997      * Installs an Icon in the project.
    998      * @return true if the icon was installed.
    999      */
   1000     private boolean installIcon(String templateFolder, String iconName, File resourceFolder,
   1001             String folderName) throws ProjectCreateException {
   1002         File icon = new File(templateFolder, iconName);
   1003         if (icon.exists()) {
   1004             File drawable = createDirs(resourceFolder, folderName);
   1005             installBinaryFile(icon, new File(drawable, "icon.png"));
   1006             return true;
   1007         }
   1008 
   1009         return false;
   1010     }
   1011 
   1012     /**
   1013      * Installs a binary file
   1014      * @param source the source file to copy
   1015      * @param destination the destination file to write
   1016      */
   1017     private void installBinaryFile(File source, File destination) {
   1018         byte[] buffer = new byte[8192];
   1019 
   1020         FileInputStream fis = null;
   1021         FileOutputStream fos = null;
   1022         try {
   1023             fis = new FileInputStream(source);
   1024             fos = new FileOutputStream(destination);
   1025 
   1026             int read;
   1027             while ((read = fis.read(buffer)) != -1) {
   1028                 fos.write(buffer, 0, read);
   1029             }
   1030 
   1031         } catch (FileNotFoundException e) {
   1032             // shouldn't happen since we check before.
   1033         } catch (IOException e) {
   1034             new ProjectCreateException(e, "Failed to read binary file: %1$s",
   1035                     source.getAbsolutePath());
   1036         } finally {
   1037             if (fis != null) {
   1038                 try {
   1039                     fis.close();
   1040                 } catch (IOException e) {
   1041                     // ignore
   1042                 }
   1043             }
   1044             if (fos != null) {
   1045                 try {
   1046                     fos.close();
   1047                 } catch (IOException e) {
   1048                     // ignore
   1049                 }
   1050             }
   1051         }
   1052 
   1053     }
   1054 
   1055     /**
   1056      * Prints a message unless silence is enabled.
   1057      * <p/>
   1058      * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
   1059      * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
   1060      *
   1061      * @param format Format for String.format
   1062      * @param args Arguments for String.format
   1063      */
   1064     private void println(String format, Object... args) {
   1065         if (mLevel != OutputLevel.SILENT) {
   1066             if (!format.endsWith("\n")) {
   1067                 format += "\n";
   1068             }
   1069             mLog.printf(format, args);
   1070         }
   1071     }
   1072 
   1073     /**
   1074      * Creates a new folder, along with any parent folders that do not exists.
   1075      *
   1076      * @param parent the parent folder
   1077      * @param name the name of the directory to create.
   1078      * @throws ProjectCreateException
   1079      */
   1080     private File createDirs(File parent, String name) throws ProjectCreateException {
   1081         final File newFolder = new File(parent, name);
   1082         boolean existedBefore = true;
   1083 
   1084         if (!newFolder.exists()) {
   1085             if (!newFolder.mkdirs()) {
   1086                 throw new ProjectCreateException("Could not create directory: %1$s", newFolder);
   1087             }
   1088             existedBefore = false;
   1089         }
   1090 
   1091         if (newFolder.isDirectory()) {
   1092             if (!newFolder.canWrite()) {
   1093                 throw new ProjectCreateException("Path is not writable: %1$s", newFolder);
   1094             }
   1095         } else {
   1096             throw new ProjectCreateException("Path is not a directory: %1$s", newFolder);
   1097         }
   1098 
   1099         if (!existedBefore) {
   1100             try {
   1101                 println("Created directory %1$s", newFolder.getCanonicalPath());
   1102             } catch (IOException e) {
   1103                 throw new ProjectCreateException(
   1104                         "Could not determine canonical path of created directory", e);
   1105             }
   1106         }
   1107 
   1108         return newFolder;
   1109     }
   1110 
   1111     /**
   1112      * Strips the string of beginning and trailing characters (multiple
   1113      * characters will be stripped, example stripString("..test...", '.')
   1114      * results in "test";
   1115      *
   1116      * @param s the string to strip
   1117      * @param strip the character to strip from beginning and end
   1118      * @return the stripped string or the empty string if everything is stripped.
   1119      */
   1120     private static String stripString(String s, char strip) {
   1121         final int sLen = s.length();
   1122         int newStart = 0, newEnd = sLen - 1;
   1123 
   1124         while (newStart < sLen && s.charAt(newStart) == strip) {
   1125           newStart++;
   1126         }
   1127         while (newEnd >= 0 && s.charAt(newEnd) == strip) {
   1128           newEnd--;
   1129         }
   1130 
   1131         /*
   1132          * newEnd contains a char we want, and substring takes end as being
   1133          * exclusive
   1134          */
   1135         newEnd++;
   1136 
   1137         if (newStart >= sLen || newEnd < 0) {
   1138             return "";
   1139         }
   1140 
   1141         return s.substring(newStart, newEnd);
   1142     }
   1143 }
   1144