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