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") ProjectCreateException(String message)118 ProjectCreateException(String message) { 119 super(message); 120 } 121 ProjectCreateException(Throwable t, String format, Object... args)122 ProjectCreateException(Throwable t, String format, Object... args) { 123 super(format != null ? String.format(format, args) : format, t); 124 } 125 ProjectCreateException(String format, Object... args)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 */ ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ISdkLog log)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 */ createProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, boolean library, String pathToMainProject)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 createExportProject(String folderPath, String projectName, String packageName)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 checkNewProjectLocation(String folderPath)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 */ updateProject(String folderPath, IAndroidTarget target, String projectName, String libraryPath)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 */ updateTestProject(final String folderPath, final String pathToMainProject, final SdkManager sdkManager)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 */ updateExportProject(String folderPath, String projectName, boolean force)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 */ checkProjectFolder(String folderPath, String requiredFilename)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 */ checkFileContainsRegexp(File file, String regexp)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 */ extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords)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 combinePackageActivityNames(String packageName, String activityName)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 */ installTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target)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 */ installTemplate(String templateName, File destFile, Map<String, String> placeholderMap)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 */ installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap)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 */ installIcons(File resourceFolder, IAndroidTarget target)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 */ installIcon(String templateFolder, String iconName, File resourceFolder, String folderName)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 */ installBinaryFile(File source, File destination)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 */ println(String format, Object... args)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 */ createDirs(File parent, String name)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 */ stripString(String s, char strip)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