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