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.internal.project.ProjectProperties.PropertyType; 23 import com.android.sdklib.xml.AndroidManifest; 24 import com.android.sdklib.xml.AndroidXPathFactory; 25 26 import org.w3c.dom.NodeList; 27 import org.xml.sax.InputSource; 28 29 import java.io.BufferedReader; 30 import java.io.BufferedWriter; 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileNotFoundException; 34 import java.io.FileOutputStream; 35 import java.io.FileReader; 36 import java.io.FileWriter; 37 import java.io.IOException; 38 import java.util.HashMap; 39 import java.util.Map; 40 import java.util.regex.Pattern; 41 42 import javax.xml.xpath.XPath; 43 import javax.xml.xpath.XPathConstants; 44 import javax.xml.xpath.XPathExpressionException; 45 46 /** 47 * Creates the basic files needed to get an Android project up and running. 48 * 49 * @hide 50 */ 51 public class ProjectCreator { 52 53 /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */ 54 private final static String PH_JAVA_FOLDER = "PACKAGE_PATH"; 55 /** Package name substitution string used in template files, i.e. "PACKAGE" */ 56 private final static String PH_PACKAGE = "PACKAGE"; 57 /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". 58 * @deprecated This is only used for older templates. For new ones see 59 * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */ 60 private final static String PH_ACTIVITY_NAME = "ACTIVITY_NAME"; 61 /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/ 62 private final static String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME"; 63 /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/ 64 private final static String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME"; 65 /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/ 66 private final static String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME"; 67 /** Original Activity class name substitution string used in class templates, i.e. 68 * "ACTIVITY_TESTED_CLASS_NAME".*/ 69 private final static String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME"; 70 /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */ 71 private final static String PH_PROJECT_NAME = "PROJECT_NAME"; 72 /** Application icon substitution string used in the manifest template */ 73 private final static String PH_ICON = "ICON"; 74 75 /** Pattern for characters accepted in a project name. Since this will be used as a 76 * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */ 77 public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+"); 78 /** List of valid characters for a project name. Used for display purposes. */ 79 public final static String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _"; 80 81 /** Pattern for characters accepted in a package name. A package is list of Java identifier 82 * separated by a dot. We need to have at least one dot (e.g. a two-level package name). 83 * A Java identifier cannot start by a digit. */ 84 public static final Pattern RE_PACKAGE_NAME = 85 Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+"); 86 /** List of valid characters for a project name. Used for display purposes. */ 87 public final static String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _"; 88 89 /** Pattern for characters accepted in an activity name, which is a Java identifier. */ 90 public static final Pattern RE_ACTIVITY_NAME = 91 Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); 92 /** List of valid characters for a project name. Used for display purposes. */ 93 public final static String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _"; 94 95 96 public enum OutputLevel { 97 /** Silent mode. Project creation will only display errors. */ 98 SILENT, 99 /** Normal mode. Project creation will display what's being done, display 100 * error but not warnings. */ 101 NORMAL, 102 /** Verbose mode. Project creation will display what's being done, errors and warnings. */ 103 VERBOSE; 104 } 105 106 /** 107 * Exception thrown when a project creation fails, typically because a template 108 * file cannot be written. 109 */ 110 private static class ProjectCreateException extends Exception { 111 /** default UID. This will not be serialized anyway. */ 112 private static final long serialVersionUID = 1L; 113 114 @SuppressWarnings("unused") ProjectCreateException(String message)115 ProjectCreateException(String message) { 116 super(message); 117 } 118 ProjectCreateException(Throwable t, String format, Object... args)119 ProjectCreateException(Throwable t, String format, Object... args) { 120 super(format != null ? String.format(format, args) : format, t); 121 } 122 ProjectCreateException(String format, Object... args)123 ProjectCreateException(String format, Object... args) { 124 super(String.format(format, args)); 125 } 126 } 127 128 private final OutputLevel mLevel; 129 130 private final ISdkLog mLog; 131 private final String mSdkFolder; 132 ProjectCreator(String sdkFolder, OutputLevel level, ISdkLog log)133 public ProjectCreator(String sdkFolder, OutputLevel level, ISdkLog log) { 134 mSdkFolder = sdkFolder; 135 mLevel = level; 136 mLog = log; 137 } 138 139 /** 140 * Creates a new project. 141 * <p/> 142 * The caller should have already checked and sanitized the parameters. 143 * 144 * @param folderPath the folder of the project to create. 145 * @param projectName the name of the project. The name must match the 146 * {@link #RE_PROJECT_NAME} regex. 147 * @param packageName the package of the project. The name must match the 148 * {@link #RE_PACKAGE_NAME} regex. 149 * @param activityEntry the activity of the project as it will appear in the manifest. Can be 150 * null if no activity should be created. The name must match the 151 * {@link #RE_ACTIVITY_NAME} regex. 152 * @param target the project target. 153 * @param pathToMainProject if non-null the project will be setup to test a main project 154 * located at the given path. 155 */ createProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, String pathToMainProject)156 public void createProject(String folderPath, String projectName, 157 String packageName, String activityEntry, IAndroidTarget target, 158 String pathToMainProject) { 159 160 // create project folder if it does not exist 161 File projectFolder = new File(folderPath); 162 if (!projectFolder.exists()) { 163 164 boolean created = false; 165 Throwable t = null; 166 try { 167 created = projectFolder.mkdirs(); 168 } catch (Exception e) { 169 t = e; 170 } 171 172 if (created) { 173 println("Created project directory: %1$s", projectFolder); 174 } else { 175 mLog.error(t, "Could not create directory: %1$s", projectFolder); 176 return; 177 } 178 } else { 179 Exception e = null; 180 String error = null; 181 try { 182 String[] content = projectFolder.list(); 183 if (content == null) { 184 error = "Project folder '%1$s' is not a directory."; 185 } else if (content.length != 0) { 186 error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead."; 187 } 188 } catch (Exception e1) { 189 e = e1; 190 } 191 192 if (e != null || error != null) { 193 mLog.error(e, error, projectFolder, SdkConstants.androidCmdName()); 194 } 195 } 196 197 try { 198 boolean isTestProject = pathToMainProject != null; 199 200 // first create the project properties. 201 202 // location of the SDK goes in localProperty 203 ProjectProperties localProperties = ProjectProperties.create(folderPath, 204 PropertyType.LOCAL); 205 localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); 206 localProperties.save(); 207 208 // target goes in default properties 209 ProjectProperties defaultProperties = ProjectProperties.create(folderPath, 210 PropertyType.DEFAULT); 211 defaultProperties.setAndroidTarget(target); 212 defaultProperties.save(); 213 214 // create a build.properties file with just the application package 215 ProjectProperties buildProperties = ProjectProperties.create(folderPath, 216 PropertyType.BUILD); 217 218 // only put application.package for older target where the rules file didn't. 219 // grab it through xpath 220 if (target.getVersion().getApiLevel() < 4) { 221 buildProperties.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE, packageName); 222 } 223 224 if (isTestProject) { 225 buildProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, 226 pathToMainProject); 227 } 228 229 buildProperties.save(); 230 231 // create the map for place-holders of values to replace in the templates 232 final HashMap<String, String> keywords = new HashMap<String, String>(); 233 234 // create the required folders. 235 // compute src folder path 236 final String packagePath = 237 stripString(packageName.replace(".", File.separator), 238 File.separatorChar); 239 240 // put this path in the place-holder map for project files that needs to list 241 // files manually. 242 keywords.put(PH_JAVA_FOLDER, packagePath); 243 keywords.put(PH_PACKAGE, packageName); 244 245 246 // compute some activity related information 247 String fqActivityName = null, activityPath = null, activityClassName = null; 248 String originalActivityEntry = activityEntry; 249 String originalActivityClassName = null; 250 if (activityEntry != null) { 251 if (isTestProject) { 252 // append Test so that it doesn't collide with the main project activity. 253 activityEntry += "Test"; 254 255 // get the classname from the original activity entry. 256 int pos = originalActivityEntry.lastIndexOf('.'); 257 if (pos != -1) { 258 originalActivityClassName = originalActivityEntry.substring(pos + 1); 259 } else { 260 originalActivityClassName = originalActivityEntry; 261 } 262 } 263 264 // get the fully qualified name of the activity 265 fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, 266 activityEntry); 267 268 // get the activity path (replace the . to /) 269 activityPath = stripString(fqActivityName.replace(".", File.separator), 270 File.separatorChar); 271 272 // remove the last segment, so that we only have the path to the activity, but 273 // not the activity filename itself. 274 activityPath = activityPath.substring(0, 275 activityPath.lastIndexOf(File.separatorChar)); 276 277 // finally, get the class name for the activity 278 activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); 279 } 280 281 // at this point we have the following for the activity: 282 // activityEntry: this is the manifest entry. For instance .MyActivity 283 // fqActivityName: full-qualified class name: com.foo.MyActivity 284 // activityClassName: only the classname: MyActivity 285 // originalActivityClassName: the classname of the activity being tested (if applicable) 286 287 // Add whatever activity info is needed in the place-holder map. 288 // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). 289 if (target.getVersion().getApiLevel() < 4) { // legacy 290 if (originalActivityEntry != null) { 291 keywords.put(PH_ACTIVITY_NAME, originalActivityEntry); 292 } 293 } else { 294 // newer templates make a difference between the manifest entries, classnames, 295 // as well as the main and test classes. 296 if (activityEntry != null) { 297 keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); 298 keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); 299 keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); 300 if (originalActivityClassName != null) { 301 keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName); 302 } 303 } 304 } 305 306 // Take the project name from the command line if there's one 307 if (projectName != null) { 308 keywords.put(PH_PROJECT_NAME, projectName); 309 } else { 310 if (activityClassName != null) { 311 // Use the activity class name as project name 312 keywords.put(PH_PROJECT_NAME, activityClassName); 313 } else { 314 // We need a project name. Just pick up the basename of the project 315 // directory. 316 projectName = projectFolder.getName(); 317 keywords.put(PH_PROJECT_NAME, projectName); 318 } 319 } 320 321 // create the source folder for the activity 322 if (activityClassName != null) { 323 String srcActivityFolderPath = SdkConstants.FD_SOURCES + File.separator + activityPath; 324 File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); 325 326 String javaTemplate = isTestProject ? "java_tests_file.template" 327 : "java_file.template"; 328 String activityFileName = activityClassName + ".java"; 329 330 installTemplate(javaTemplate, new File(sourceFolder, activityFileName), 331 keywords, target); 332 } else { 333 // we should at least create 'src' 334 createDirs(projectFolder, SdkConstants.FD_SOURCES); 335 } 336 337 // create other useful folders 338 File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES); 339 createDirs(projectFolder, SdkConstants.FD_OUTPUT); 340 createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS); 341 342 if (isTestProject == false) { 343 /* Make res files only for non test projects */ 344 File valueFolder = createDirs(resourceFolder, SdkConstants.FD_VALUES); 345 installTemplate("strings.template", new File(valueFolder, "strings.xml"), 346 keywords, target); 347 348 File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_LAYOUT); 349 installTemplate("layout.template", new File(layoutFolder, "main.xml"), 350 keywords, target); 351 352 // create the icons 353 if (installIcons(resourceFolder, target)) { 354 keywords.put(PH_ICON, "android:icon=\"@drawable/icon\""); 355 } else { 356 keywords.put(PH_ICON, ""); 357 } 358 } 359 360 /* Make AndroidManifest.xml and build.xml files */ 361 String manifestTemplate = "AndroidManifest.template"; 362 if (isTestProject) { 363 manifestTemplate = "AndroidManifest.tests.template"; 364 } 365 366 installTemplate(manifestTemplate, 367 new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML), 368 keywords, target); 369 370 installTemplate("build.template", 371 new File(projectFolder, SdkConstants.FN_BUILD_XML), 372 keywords); 373 } catch (ProjectCreateException e) { 374 mLog.error(e, null); 375 } catch (IOException e) { 376 mLog.error(e, null); 377 } 378 } 379 380 /** 381 * Updates an existing project. 382 * <p/> 383 * Workflow: 384 * <ul> 385 * <li> Check AndroidManifest.xml is present (required) 386 * <li> Check there's a default.properties with a target *or* --target was specified 387 * <li> Update default.prop if --target was specified 388 * <li> Refresh/create "sdk" in local.properties 389 * <li> Build.xml: create if not present or no <androidinit(\w|/>) in it 390 * </ul> 391 * 392 * @param folderPath the folder of the project to update. This folder must exist. 393 * @param target the project target. Can be null. 394 * @param projectName The project name from --name. Can be null. 395 */ updateProject(String folderPath, IAndroidTarget target, String projectName)396 public void updateProject(String folderPath, IAndroidTarget target, String projectName) { 397 // since this is an update, check the folder does point to a project 398 File androidManifest = checkProjectFolder(folderPath); 399 if (androidManifest == null) { 400 return; 401 } 402 403 // get the parent File. 404 File projectFolder = androidManifest.getParentFile(); 405 406 // Check there's a default.properties with a target *or* --target was specified 407 ProjectProperties props = ProjectProperties.load(folderPath, PropertyType.DEFAULT); 408 if (props == null || props.getProperty(ProjectProperties.PROPERTY_TARGET) == null) { 409 if (target == null) { 410 mLog.error(null, 411 "There is no %1$s file in '%2$s'. Please provide a --target to the '%3$s update' command.", 412 PropertyType.DEFAULT.getFilename(), 413 folderPath, 414 SdkConstants.androidCmdName()); 415 return; 416 } 417 } 418 419 // Update default.prop if --target was specified 420 if (target != null) { 421 // we already attempted to load the file earlier, if that failed, create it. 422 if (props == null) { 423 props = ProjectProperties.create(folderPath, PropertyType.DEFAULT); 424 } 425 426 // set or replace the target 427 props.setAndroidTarget(target); 428 try { 429 props.save(); 430 println("Updated %1$s", PropertyType.DEFAULT.getFilename()); 431 } catch (IOException e) { 432 mLog.error(e, "Failed to write %1$s file in '%2$s'", 433 PropertyType.DEFAULT.getFilename(), 434 folderPath); 435 return; 436 } 437 } 438 439 // Refresh/create "sdk" in local.properties 440 // because the file may already exists and contain other values (like apk config), 441 // we first try to load it. 442 props = ProjectProperties.load(folderPath, PropertyType.LOCAL); 443 if (props == null) { 444 props = ProjectProperties.create(folderPath, PropertyType.LOCAL); 445 } 446 447 // set or replace the sdk location. 448 props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); 449 try { 450 props.save(); 451 println("Updated %1$s", PropertyType.LOCAL.getFilename()); 452 } catch (IOException e) { 453 mLog.error(e, "Failed to write %1$s file in '%2$s'", 454 PropertyType.LOCAL.getFilename(), 455 folderPath); 456 return; 457 } 458 459 // Build.xml: create if not present or no <androidinit/> in it 460 File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); 461 boolean needsBuildXml = projectName != null || !buildXml.exists(); 462 if (!needsBuildXml) { 463 // Look for for a classname="com.android.ant.SetupTask" attribute 464 needsBuildXml = !checkFileContainsRegexp(buildXml, 465 "classname=\"com.android.ant.SetupTask\""); //$NON-NLS-1$ 466 } 467 if (!needsBuildXml) { 468 // Note that "<setup" must be followed by either a whitespace, a "/" (for the 469 // XML /> closing tag) or an end-of-line. This way we know the XML tag is really this 470 // one and later we will be able to use an "androidinit2" tag or such as necessary. 471 needsBuildXml = !checkFileContainsRegexp(buildXml, "<setup(?:\\s|/|$)"); //$NON-NLS-1$ 472 } 473 if (needsBuildXml) { 474 println("File %1$s is too old and needs to be updated.", SdkConstants.FN_BUILD_XML); 475 } 476 477 if (needsBuildXml) { 478 // create the map for place-holders of values to replace in the templates 479 final HashMap<String, String> keywords = new HashMap<String, String>(); 480 481 // Take the project name from the command line if there's one 482 if (projectName != null) { 483 keywords.put(PH_PROJECT_NAME, projectName); 484 } else { 485 extractPackageFromManifest(androidManifest, keywords); 486 if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) { 487 String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME); 488 // keep only the last segment if applicable 489 int pos = activity.lastIndexOf('.'); 490 if (pos != -1) { 491 activity = activity.substring(pos + 1); 492 } 493 494 // Use the activity as project name 495 keywords.put(PH_PROJECT_NAME, activity); 496 } else { 497 // We need a project name. Just pick up the basename of the project 498 // directory. 499 projectName = projectFolder.getName(); 500 keywords.put(PH_PROJECT_NAME, projectName); 501 } 502 } 503 504 if (mLevel == OutputLevel.VERBOSE) { 505 println("Regenerating %1$s with project name %2$s", 506 SdkConstants.FN_BUILD_XML, 507 keywords.get(PH_PROJECT_NAME)); 508 } 509 510 try { 511 installTemplate("build.template", 512 new File(projectFolder, SdkConstants.FN_BUILD_XML), 513 keywords); 514 } catch (ProjectCreateException e) { 515 mLog.error(e, null); 516 } 517 } 518 } 519 520 /** 521 * Updates a test project with a new path to the main (tested) project. 522 * @param folderPath the path of the test project. 523 * @param pathToMainProject the path to the main project, relative to the test project. 524 */ updateTestProject(final String folderPath, final String pathToMainProject)525 public void updateTestProject(final String folderPath, final String pathToMainProject) { 526 // since this is an update, check the folder does point to a project 527 if (checkProjectFolder(folderPath) == null) { 528 return; 529 } 530 531 // check the path to the main project is valid. 532 File mainProject = new File(pathToMainProject); 533 String resolvedPath; 534 if (mainProject.isAbsolute() == false) { 535 mainProject = new File(folderPath, pathToMainProject); 536 try { 537 resolvedPath = mainProject.getCanonicalPath(); 538 } catch (IOException e) { 539 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject); 540 return; 541 } 542 } else { 543 resolvedPath = mainProject.getAbsolutePath(); 544 } 545 546 println("Resolved location of main project to: %1$s", resolvedPath); 547 548 // check the main project exists 549 if (checkProjectFolder(resolvedPath) == null) { 550 return; 551 } 552 553 ProjectProperties props = ProjectProperties.create(folderPath, PropertyType.BUILD); 554 555 // set or replace the path to the main project 556 props.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); 557 try { 558 props.save(); 559 println("Updated %1$s", PropertyType.BUILD.getFilename()); 560 } catch (IOException e) { 561 mLog.error(e, "Failed to write %1$s file in '%2$s'", 562 PropertyType.BUILD.getFilename(), 563 folderPath); 564 return; 565 } 566 } 567 568 /** 569 * Checks whether the give <var>folderPath</var> is a valid project folder, and returns 570 * a {@link File} to the AndroidManifest.xml file. 571 * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it. 572 * <p/>Any error are output using {@link #mLog}. 573 * @param folderPath the folder to check 574 * @return a {@link File} to the AndroidManifest.xml file, or null otherwise. 575 */ checkProjectFolder(String folderPath)576 private File checkProjectFolder(String folderPath) { 577 // project folder must exist and be a directory, since this is an update 578 File projectFolder = new File(folderPath); 579 if (!projectFolder.isDirectory()) { 580 mLog.error(null, "Project folder '%1$s' is not a valid directory, this is not an Android project you can update.", 581 projectFolder); 582 return null; 583 } 584 585 // Check AndroidManifest.xml is present 586 File androidManifest = new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML); 587 if (!androidManifest.isFile()) { 588 mLog.error(null, 589 "%1$s not found in '%2$s', this is not an Android project you can update.", 590 SdkConstants.FN_ANDROID_MANIFEST_XML, 591 folderPath); 592 return null; 593 } 594 595 return androidManifest; 596 } 597 598 /** 599 * Returns true if any line of the input file contains the requested regexp. 600 */ checkFileContainsRegexp(File file, String regexp)601 private boolean checkFileContainsRegexp(File file, String regexp) { 602 Pattern p = Pattern.compile(regexp); 603 604 try { 605 BufferedReader in = new BufferedReader(new FileReader(file)); 606 String line; 607 608 while ((line = in.readLine()) != null) { 609 if (p.matcher(line).find()) { 610 return true; 611 } 612 } 613 614 in.close(); 615 } catch (Exception e) { 616 // ignore 617 } 618 619 return false; 620 } 621 622 /** 623 * Extracts a "full" package & activity name from an AndroidManifest.xml. 624 * <p/> 625 * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. 626 * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}. 627 * When no activity is found, this key is not created. 628 * 629 * @param manifestFile The AndroidManifest.xml file 630 * @param outKeywords Place where to put the out parameters: package and activity names. 631 * @return True if the package/activity was parsed and updated in the keyword dictionary. 632 */ extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords)633 private boolean extractPackageFromManifest(File manifestFile, 634 Map<String, String> outKeywords) { 635 try { 636 XPath xpath = AndroidXPathFactory.newXPath(); 637 638 InputSource source = new InputSource(new FileReader(manifestFile)); 639 String packageName = xpath.evaluate("/manifest/@package", source); 640 641 source = new InputSource(new FileReader(manifestFile)); 642 643 // Select the "android:name" attribute of all <activity> nodes but only if they 644 // contain a sub-node <intent-filter><action> with an "android:name" attribute which 645 // is 'android.intent.action.MAIN' and an <intent-filter><category> with an 646 // "android:name" attribute which is 'android.intent.category.LAUNCHER' 647 String expression = String.format("/manifest/application/activity" + 648 "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + 649 "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + 650 "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX); 651 652 NodeList activityNames = (NodeList) xpath.evaluate(expression, source, 653 XPathConstants.NODESET); 654 655 // If we get here, both XPath expressions were valid so we're most likely dealing 656 // with an actual AndroidManifest.xml file. The nodes may not have the requested 657 // attributes though, if which case we should warn. 658 659 if (packageName == null || packageName.length() == 0) { 660 mLog.error(null, 661 "Missing <manifest package=\"...\"> in '%1$s'", 662 manifestFile.getName()); 663 return false; 664 } 665 666 // Get the first activity that matched earlier. If there is no activity, 667 // activityName is set to an empty string and the generated "combined" name 668 // will be in the form "package." (with a dot at the end). 669 String activityName = ""; 670 if (activityNames.getLength() > 0) { 671 activityName = activityNames.item(0).getNodeValue(); 672 } 673 674 if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { 675 println("WARNING: There is more than one activity defined in '%1$s'.\n" + 676 "Only the first one will be used. If this is not appropriate, you need\n" + 677 "to specify one of these values manually instead:", 678 manifestFile.getName()); 679 680 for (int i = 0; i < activityNames.getLength(); i++) { 681 String name = activityNames.item(i).getNodeValue(); 682 name = combinePackageActivityNames(packageName, name); 683 println("- %1$s", name); 684 } 685 } 686 687 if (activityName.length() == 0) { 688 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" + 689 "No activity will be generated.", 690 AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName()); 691 } else { 692 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName); 693 } 694 695 outKeywords.put(PH_PACKAGE, packageName); 696 return true; 697 698 } catch (IOException e) { 699 mLog.error(e, "Failed to read %1$s", manifestFile.getName()); 700 } catch (XPathExpressionException e) { 701 Throwable t = e.getCause(); 702 mLog.error(t == null ? e : t, 703 "Failed to parse %1$s", 704 manifestFile.getName()); 705 } 706 707 return false; 708 } 709 combinePackageActivityNames(String packageName, String activityName)710 private String combinePackageActivityNames(String packageName, String activityName) { 711 // Activity Name can have 3 forms: 712 // - ".Name" means this is a class name in the given package name. 713 // The full FQCN is thus packageName + ".Name" 714 // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" 715 // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. 716 // To be valid, the package name should have at least two components. This is checked 717 // later during the creation of the build.xml file, so we just need to detect there's 718 // a dot but not at pos==0. 719 720 int pos = activityName.indexOf('.'); 721 if (pos == 0) { 722 return packageName + activityName; 723 } else if (pos > 0) { 724 return activityName; 725 } else { 726 return packageName + "." + activityName; 727 } 728 } 729 730 /** 731 * Installs a new file that is based on a template file provided by a given target. 732 * Each match of each key from the place-holder map in the template will be replaced with its 733 * corresponding value in the created file. 734 * 735 * @param templateName the name of to the template file 736 * @param destFile the path to the destination file, relative to the project 737 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 738 * @param target the Target of the project that will be providing the template. 739 * @throws ProjectCreateException 740 */ installTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target)741 private void installTemplate(String templateName, File destFile, 742 Map<String, String> placeholderMap, IAndroidTarget target) 743 throws ProjectCreateException { 744 // query the target for its template directory 745 String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); 746 final String sourcePath = templateFolder + File.separator + templateName; 747 748 installFullPathTemplate(sourcePath, destFile, placeholderMap); 749 } 750 751 /** 752 * Installs a new file that is based on a template file provided by the tools folder. 753 * Each match of each key from the place-holder map in the template will be replaced with its 754 * corresponding value in the created file. 755 * 756 * @param templateName the name of to the template file 757 * @param destFile the path to the destination file, relative to the project 758 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 759 * @throws ProjectCreateException 760 */ installTemplate(String templateName, File destFile, Map<String, String> placeholderMap)761 private void installTemplate(String templateName, File destFile, 762 Map<String, String> placeholderMap) 763 throws ProjectCreateException { 764 // query the target for its template directory 765 String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; 766 final String sourcePath = templateFolder + File.separator + templateName; 767 768 installFullPathTemplate(sourcePath, destFile, placeholderMap); 769 } 770 771 /** 772 * Installs a new file that is based on a template. 773 * Each match of each key from the place-holder map in the template will be replaced with its 774 * corresponding value in the created file. 775 * 776 * @param sourcePath the full path to the source template file 777 * @param destFile the destination file 778 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 779 * @throws ProjectCreateException 780 */ installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap)781 private void installFullPathTemplate(String sourcePath, File destFile, 782 Map<String, String> placeholderMap) throws ProjectCreateException { 783 784 boolean existed = destFile.exists(); 785 786 try { 787 BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); 788 BufferedReader in = new BufferedReader(new FileReader(sourcePath)); 789 String line; 790 791 while ((line = in.readLine()) != null) { 792 if (placeholderMap != null) { 793 for (String key : placeholderMap.keySet()) { 794 line = line.replace(key, placeholderMap.get(key)); 795 } 796 } 797 798 out.write(line); 799 out.newLine(); 800 } 801 802 out.close(); 803 in.close(); 804 } catch (Exception e) { 805 throw new ProjectCreateException(e, "Could not access %1$s: %2$s", 806 destFile, e.getMessage()); 807 } 808 809 println("%1$s file %2$s", 810 existed ? "Updated" : "Added", 811 destFile); 812 } 813 814 /** 815 * Installs the project icons. 816 * @param resourceFolder the resource folder 817 * @param target the target of the project. 818 * @return true if any icon was installed. 819 */ installIcons(File resourceFolder, IAndroidTarget target)820 private boolean installIcons(File resourceFolder, IAndroidTarget target) 821 throws ProjectCreateException { 822 // query the target for its template directory 823 String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); 824 825 boolean installedIcon = false; 826 827 installedIcon |= installIcon(templateFolder, "icon_hdpi.png", resourceFolder, "drawable-hdpi"); 828 installedIcon |= installIcon(templateFolder, "icon_mdpi.png", resourceFolder, "drawable-mdpi"); 829 installedIcon |= installIcon(templateFolder, "icon_ldpi.png", resourceFolder, "drawable-ldpi"); 830 831 return installedIcon; 832 } 833 834 /** 835 * Installs an Icon in the project. 836 * @return true if the icon was installed. 837 */ installIcon(String templateFolder, String iconName, File resourceFolder, String folderName)838 private boolean installIcon(String templateFolder, String iconName, File resourceFolder, 839 String folderName) throws ProjectCreateException { 840 File icon = new File(templateFolder, iconName); 841 if (icon.exists()) { 842 File drawable = createDirs(resourceFolder, folderName); 843 installBinaryFile(icon, new File(drawable, "icon.png")); 844 return true; 845 } 846 847 return false; 848 } 849 850 /** 851 * Installs a binary file 852 * @param source the source file to copy 853 * @param destination the destination file to write 854 */ installBinaryFile(File source, File destination)855 private void installBinaryFile(File source, File destination) { 856 byte[] buffer = new byte[8192]; 857 858 FileInputStream fis = null; 859 FileOutputStream fos = null; 860 try { 861 fis = new FileInputStream(source); 862 fos = new FileOutputStream(destination); 863 864 int read; 865 while ((read = fis.read(buffer)) != -1) { 866 fos.write(buffer, 0, read); 867 } 868 869 } catch (FileNotFoundException e) { 870 // shouldn't happen since we check before. 871 } catch (IOException e) { 872 new ProjectCreateException(e, "Failed to read binary file: %1$s", 873 source.getAbsolutePath()); 874 } finally { 875 if (fis != null) { 876 try { 877 fis.close(); 878 } catch (IOException e) { 879 // ignore 880 } 881 } 882 if (fos != null) { 883 try { 884 fos.close(); 885 } catch (IOException e) { 886 // ignore 887 } 888 } 889 } 890 891 } 892 893 /** 894 * Prints a message unless silence is enabled. 895 * <p/> 896 * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from 897 * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}. 898 * 899 * @param format Format for String.format 900 * @param args Arguments for String.format 901 */ println(String format, Object... args)902 private void println(String format, Object... args) { 903 if (mLevel != OutputLevel.SILENT) { 904 if (!format.endsWith("\n")) { 905 format += "\n"; 906 } 907 mLog.printf(format, args); 908 } 909 } 910 911 /** 912 * Creates a new folder, along with any parent folders that do not exists. 913 * 914 * @param parent the parent folder 915 * @param name the name of the directory to create. 916 * @throws ProjectCreateException 917 */ createDirs(File parent, String name)918 private File createDirs(File parent, String name) throws ProjectCreateException { 919 final File newFolder = new File(parent, name); 920 boolean existedBefore = true; 921 922 if (!newFolder.exists()) { 923 if (!newFolder.mkdirs()) { 924 throw new ProjectCreateException("Could not create directory: %1$s", newFolder); 925 } 926 existedBefore = false; 927 } 928 929 if (newFolder.isDirectory()) { 930 if (!newFolder.canWrite()) { 931 throw new ProjectCreateException("Path is not writable: %1$s", newFolder); 932 } 933 } else { 934 throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); 935 } 936 937 if (!existedBefore) { 938 try { 939 println("Created directory %1$s", newFolder.getCanonicalPath()); 940 } catch (IOException e) { 941 throw new ProjectCreateException( 942 "Could not determine canonical path of created directory", e); 943 } 944 } 945 946 return newFolder; 947 } 948 949 /** 950 * Strips the string of beginning and trailing characters (multiple 951 * characters will be stripped, example stripString("..test...", '.') 952 * results in "test"; 953 * 954 * @param s the string to strip 955 * @param strip the character to strip from beginning and end 956 * @return the stripped string or the empty string if everything is stripped. 957 */ stripString(String s, char strip)958 private static String stripString(String s, char strip) { 959 final int sLen = s.length(); 960 int newStart = 0, newEnd = sLen - 1; 961 962 while (newStart < sLen && s.charAt(newStart) == strip) { 963 newStart++; 964 } 965 while (newEnd >= 0 && s.charAt(newEnd) == strip) { 966 newEnd--; 967 } 968 969 /* 970 * newEnd contains a char we want, and substring takes end as being 971 * exclusive 972 */ 973 newEnd++; 974 975 if (newStart >= sLen || newEnd < 0) { 976 return ""; 977 } 978 979 return s.substring(newStart, newEnd); 980 } 981 } 982