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