1 /* 2 * Copyright (C) 2008 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.sdkmanager; 18 19 import com.android.prefs.AndroidLocation; 20 import com.android.prefs.AndroidLocation.AndroidLocationException; 21 import com.android.sdklib.IAndroidTarget; 22 import com.android.sdklib.ISdkLog; 23 import com.android.sdklib.SdkConstants; 24 import com.android.sdklib.SdkManager; 25 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 26 import com.android.sdklib.internal.avd.AvdManager; 27 import com.android.sdklib.internal.avd.HardwareProperties; 28 import com.android.sdklib.internal.avd.AvdManager.AvdInfo; 29 import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty; 30 import com.android.sdklib.internal.project.ProjectCreator; 31 import com.android.sdklib.internal.project.ProjectProperties; 32 import com.android.sdklib.internal.project.ProjectCreator.OutputLevel; 33 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 34 import com.android.sdklib.repository.SdkRepository; 35 import com.android.sdklib.xml.AndroidXPathFactory; 36 import com.android.sdkmanager.internal.repository.AboutPage; 37 import com.android.sdkmanager.internal.repository.SettingsPage; 38 import com.android.sdkuilib.internal.repository.UpdateNoWindow; 39 import com.android.sdkuilib.internal.repository.LocalPackagesPage; 40 import com.android.sdkuilib.internal.widgets.MessageBoxLog; 41 import com.android.sdkuilib.repository.UpdaterWindow; 42 43 import org.eclipse.swt.widgets.Display; 44 import org.xml.sax.InputSource; 45 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.FileNotFoundException; 49 import java.io.IOException; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.HashMap; 53 import java.util.Map; 54 55 import javax.xml.xpath.XPath; 56 import javax.xml.xpath.XPathExpressionException; 57 58 /** 59 * Main class for the 'android' application. 60 */ 61 public class Main { 62 63 /** Java property that defines the location of the sdk/tools directory. */ 64 public final static String TOOLSDIR = "com.android.sdkmanager.toolsdir"; 65 /** Java property that defines the working directory. On Windows the current working directory 66 * is actually the tools dir, in which case this is used to get the original CWD. */ 67 private final static String WORKDIR = "com.android.sdkmanager.workdir"; 68 69 /** Value returned by {@link #resolveTargetName(String)} when the target id does not match. */ 70 private final static int INVALID_TARGET_ID = 0; 71 72 private final static String[] BOOLEAN_YES_REPLIES = new String[] { "yes", "y" }; 73 private final static String[] BOOLEAN_NO_REPLIES = new String[] { "no", "n" }; 74 75 /** Path to the SDK folder. This is the parent of {@link #TOOLSDIR}. */ 76 private String mOsSdkFolder; 77 /** Logger object. Use this to print normal output, warnings or errors. */ 78 private ISdkLog mSdkLog; 79 /** The SDK manager parses the SDK folder and gives access to the content. */ 80 private SdkManager mSdkManager; 81 /** Command-line processor with options specific to SdkManager. */ 82 private SdkCommandLine mSdkCommandLine; 83 /** The working directory, either null or set to an existing absolute canonical directory. */ 84 private File mWorkDir; 85 main(String[] args)86 public static void main(String[] args) { 87 new Main().run(args); 88 } 89 90 /** 91 * Runs the sdk manager app 92 */ run(String[] args)93 private void run(String[] args) { 94 createLogger(); 95 init(); 96 mSdkCommandLine.parseArgs(args); 97 parseSdk(); 98 doAction(); 99 } 100 101 /** 102 * Creates the {@link #mSdkLog} object. 103 * This must be done before {@link #init()} as it will be used to report errors. 104 * This logger prints to the attached console. 105 */ createLogger()106 private void createLogger() { 107 mSdkLog = new ISdkLog() { 108 public void error(Throwable t, String errorFormat, Object... args) { 109 if (errorFormat != null) { 110 System.err.printf("Error: " + errorFormat, args); 111 if (!errorFormat.endsWith("\n")) { 112 System.err.printf("\n"); 113 } 114 } 115 if (t != null) { 116 System.err.printf("Error: %s\n", t.getMessage()); 117 } 118 } 119 120 public void warning(String warningFormat, Object... args) { 121 if (mSdkCommandLine.isVerbose()) { 122 System.out.printf("Warning: " + warningFormat, args); 123 if (!warningFormat.endsWith("\n")) { 124 System.out.printf("\n"); 125 } 126 } 127 } 128 129 public void printf(String msgFormat, Object... args) { 130 System.out.printf(msgFormat, args); 131 } 132 }; 133 } 134 135 /** 136 * Init the application by making sure the SDK path is available and 137 * doing basic parsing of the SDK. 138 */ init()139 private void init() { 140 mSdkCommandLine = new SdkCommandLine(mSdkLog); 141 142 // We get passed a property for the tools dir 143 String toolsDirProp = System.getProperty(TOOLSDIR); 144 if (toolsDirProp == null) { 145 // for debugging, it's easier to override using the process environment 146 toolsDirProp = System.getenv(TOOLSDIR); 147 } 148 149 if (toolsDirProp != null) { 150 // got back a level for the SDK folder 151 File tools; 152 if (toolsDirProp.length() > 0) { 153 tools = new File(toolsDirProp); 154 mOsSdkFolder = tools.getParent(); 155 } else { 156 try { 157 tools = new File(".").getCanonicalFile(); 158 mOsSdkFolder = tools.getParent(); 159 } catch (IOException e) { 160 // Will print an error below since mSdkFolder is not defined 161 } 162 } 163 } 164 165 if (mOsSdkFolder == null) { 166 errorAndExit("The tools directory property is not set, please make sure you are executing %1$s", 167 SdkConstants.androidCmdName()); 168 } 169 170 // We might get passed a property for the working directory 171 // Either it is a valid directory and mWorkDir is set to it's absolute canonical value 172 // or mWorkDir remains null. 173 String workDirProp = System.getProperty(WORKDIR); 174 if (workDirProp == null) { 175 workDirProp = System.getenv(WORKDIR); 176 } 177 if (workDirProp != null) { 178 // This should be a valid directory 179 mWorkDir = new File(workDirProp); 180 try { 181 mWorkDir = mWorkDir.getCanonicalFile().getAbsoluteFile(); 182 } catch (IOException e) { 183 mWorkDir = null; 184 } 185 if (mWorkDir == null || !mWorkDir.isDirectory()) { 186 errorAndExit("The working directory does not seem to be valid: '%1$s", workDirProp); 187 } 188 } 189 } 190 191 /** 192 * Does the basic SDK parsing required for all actions 193 */ parseSdk()194 private void parseSdk() { 195 mSdkManager = SdkManager.createManager(mOsSdkFolder, mSdkLog); 196 197 if (mSdkManager == null) { 198 errorAndExit("Unable to parse SDK content."); 199 } 200 } 201 202 /** 203 * Actually do an action... 204 */ doAction()205 private void doAction() { 206 String verb = mSdkCommandLine.getVerb(); 207 String directObject = mSdkCommandLine.getDirectObject(); 208 209 if (SdkCommandLine.VERB_LIST.equals(verb)) { 210 // list action. 211 if (SdkCommandLine.OBJECT_TARGET.equals(directObject)) { 212 displayTargetList(); 213 214 } else if (SdkCommandLine.OBJECT_AVD.equals(directObject)) { 215 displayAvdList(); 216 217 } else { 218 displayTargetList(); 219 displayAvdList(); 220 } 221 222 } else if (SdkCommandLine.VERB_CREATE.equals(verb)) { 223 if (SdkCommandLine.OBJECT_AVD.equals(directObject)) { 224 createAvd(); 225 226 } else if (SdkCommandLine.OBJECT_PROJECT.equals(directObject)) { 227 createProject(false /*library*/); 228 229 } else if (SdkCommandLine.OBJECT_TEST_PROJECT.equals(directObject)) { 230 createTestProject(); 231 232 } else if (SdkCommandLine.OBJECT_LIB_PROJECT.equals(directObject)) { 233 createProject(true /*library*/); 234 235 } else if (SdkCommandLine.OBJECT_EXPORT_PROJECT.equals(directObject)) { 236 createExportProject(); 237 238 } 239 } else if (SdkCommandLine.VERB_UPDATE.equals(verb)) { 240 if (SdkCommandLine.OBJECT_AVD.equals(directObject)) { 241 updateAvd(); 242 243 } else if (SdkCommandLine.OBJECT_PROJECT.equals(directObject)) { 244 updateProject(false /*library*/); 245 246 } else if (SdkCommandLine.OBJECT_TEST_PROJECT.equals(directObject)) { 247 updateTestProject(); 248 249 } else if (SdkCommandLine.OBJECT_LIB_PROJECT.equals(directObject)) { 250 updateProject(true /*library*/); 251 252 } else if (SdkCommandLine.OBJECT_EXPORT_PROJECT.equals(directObject)) { 253 updateExportProject(); 254 255 } else if (SdkCommandLine.OBJECT_SDK.equals(directObject)) { 256 if (mSdkCommandLine.getFlagNoUI()) { 257 updateSdkNoUI(); 258 } else { 259 showMainWindow(true /*autoUpdate*/); 260 } 261 262 } else if (SdkCommandLine.OBJECT_ADB.equals(directObject)) { 263 updateAdb(); 264 265 } 266 } else if (SdkCommandLine.VERB_DELETE.equals(verb) && 267 SdkCommandLine.OBJECT_AVD.equals(directObject)) { 268 deleteAvd(); 269 270 } else if (SdkCommandLine.VERB_MOVE.equals(verb) && 271 SdkCommandLine.OBJECT_AVD.equals(directObject)) { 272 moveAvd(); 273 274 } else if (verb == null && directObject == null) { 275 showMainWindow(false /*autoUpdate*/); 276 277 } else { 278 mSdkCommandLine.printHelpAndExit(null); 279 } 280 } 281 282 /** 283 * Display the main SdkManager app window 284 */ showMainWindow(boolean autoUpdate)285 private void showMainWindow(boolean autoUpdate) { 286 try { 287 // display a message talking about the command line version 288 System.out.printf("No command line parameters provided, launching UI.\n" + 289 "See 'android --help' for operations from the command line.\n"); 290 291 MessageBoxLog errorLogger = new MessageBoxLog( 292 "SDK Manager", 293 Display.getCurrent(), 294 true /*logErrorsOnly*/); 295 296 UpdaterWindow window = new UpdaterWindow( 297 null /* parentShell */, 298 errorLogger, 299 mOsSdkFolder, 300 false /*userCanChangeSdkRoot*/); 301 window.registerPage("Settings", SettingsPage.class); 302 window.registerPage("About", AboutPage.class); 303 if (autoUpdate) { 304 window.setInitialPage(LocalPackagesPage.class); 305 window.setRequestAutoUpdate(true); 306 } 307 window.open(); 308 309 errorLogger.displayResult(true); 310 311 } catch (Exception e) { 312 e.printStackTrace(); 313 } 314 } 315 316 /** 317 * Updates the whole SDK without any UI, just using console output. 318 */ updateSdkNoUI()319 private void updateSdkNoUI() { 320 boolean force = mSdkCommandLine.getFlagForce(); 321 boolean useHttp = mSdkCommandLine.getFlagNoHttps(); 322 boolean dryMode = mSdkCommandLine.getFlagDryMode(); 323 boolean obsolete = mSdkCommandLine.getFlagObsolete(); 324 325 // Check filter types. 326 ArrayList<String> pkgFilter = new ArrayList<String>(); 327 String filter = mSdkCommandLine.getParamFilter(); 328 if (filter != null && filter.length() > 0) { 329 for (String t : filter.split(",")) { //$NON-NLS-1$ 330 if (t != null) { 331 t = t.trim(); 332 if (t.length() > 0) { 333 boolean found = false; 334 for (String t2 : SdkRepository.NODES) { 335 if (t2.equals(t)) { 336 pkgFilter.add(t2); 337 found = true; 338 break; 339 } 340 } 341 if (!found) { 342 errorAndExit( 343 "Unknown package filter type '%1$s'.\nAccepted values are: %2$s", 344 t, 345 Arrays.toString(SdkRepository.NODES)); 346 return; 347 } 348 } 349 } 350 } 351 } 352 353 UpdateNoWindow upd = new UpdateNoWindow(mOsSdkFolder, mSdkManager, mSdkLog, force, useHttp); 354 upd.updateAll(pkgFilter, obsolete, dryMode); 355 } 356 357 /** 358 * Returns a configured {@link ProjectCreator} instance. 359 */ getProjectCreator()360 private ProjectCreator getProjectCreator() { 361 ProjectCreator creator = new ProjectCreator(mSdkManager, mOsSdkFolder, 362 mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : 363 mSdkCommandLine.isSilent() ? OutputLevel.SILENT : 364 OutputLevel.NORMAL, 365 mSdkLog); 366 return creator; 367 } 368 369 370 /** 371 * Creates a new Android project based on command-line parameters 372 */ createProject(boolean library)373 private void createProject(boolean library) { 374 String directObject = library? SdkCommandLine.OBJECT_LIB_PROJECT : 375 SdkCommandLine.OBJECT_PROJECT; 376 377 // get the target and try to resolve it. 378 int targetId = resolveTargetName(mSdkCommandLine.getParamTargetId()); 379 IAndroidTarget[] targets = mSdkManager.getTargets(); 380 if (targetId == INVALID_TARGET_ID || targetId > targets.length) { 381 errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", 382 SdkConstants.androidCmdName()); 383 } 384 IAndroidTarget target = targets[targetId - 1]; // target id is 1-based 385 386 ProjectCreator creator = getProjectCreator(); 387 388 String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); 389 390 String projectName = mSdkCommandLine.getParamName(); 391 String packageName = mSdkCommandLine.getParamProjectPackage(directObject); 392 String activityName = null; 393 if (library == false) { 394 activityName = mSdkCommandLine.getParamProjectActivity(); 395 } 396 397 if (projectName != null && 398 !ProjectCreator.RE_PROJECT_NAME.matcher(projectName).matches()) { 399 errorAndExit( 400 "Project name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", 401 projectName, ProjectCreator.CHARS_PROJECT_NAME); 402 return; 403 } 404 405 if (activityName != null && 406 !ProjectCreator.RE_ACTIVITY_NAME.matcher(activityName).matches()) { 407 errorAndExit( 408 "Activity name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", 409 activityName, ProjectCreator.CHARS_ACTIVITY_NAME); 410 return; 411 } 412 413 if (packageName != null && 414 !ProjectCreator.RE_PACKAGE_NAME.matcher(packageName).matches()) { 415 errorAndExit( 416 "Package name '%1$s' contains invalid characters.\n" + 417 "A package name must be constitued of two Java identifiers.\n" + 418 "Each identifier allowed characters are: %2$s", 419 packageName, ProjectCreator.CHARS_PACKAGE_NAME); 420 return; 421 } 422 423 creator.createProject(projectDir, 424 projectName, 425 packageName, 426 activityName, 427 target, 428 library, 429 null /*pathToMain*/); 430 } 431 432 /** 433 * Creates a new Android test project based on command-line parameters 434 */ createTestProject()435 private void createTestProject() { 436 437 String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); 438 439 // first check the path of the parent project, and make sure it's valid. 440 String pathToMainProject = mSdkCommandLine.getParamTestProjectMain(); 441 442 File parentProject = new File(pathToMainProject); 443 if (parentProject.isAbsolute() == false) { 444 // if the path is not absolute, we need to resolve it based on the 445 // destination path of the project 446 try { 447 parentProject = new File(projectDir, pathToMainProject).getCanonicalFile(); 448 } catch (IOException e) { 449 errorAndExit("Unable to resolve Main project's directory: %1$s", 450 pathToMainProject); 451 return; // help Eclipse static analyzer understand we'll never execute the rest. 452 } 453 } 454 455 if (parentProject.isDirectory() == false) { 456 errorAndExit("Main project's directory does not exist: %1$s", 457 pathToMainProject); 458 return; 459 } 460 461 // now look for a manifest in there 462 File manifest = new File(parentProject, SdkConstants.FN_ANDROID_MANIFEST_XML); 463 if (manifest.isFile() == false) { 464 errorAndExit("No AndroidManifest.xml file found in the main project directory: %1$s", 465 parentProject.getAbsolutePath()); 466 return; 467 } 468 469 // now query the manifest for the package file. 470 XPath xpath = AndroidXPathFactory.newXPath(); 471 String packageName, activityName; 472 473 try { 474 packageName = xpath.evaluate("/manifest/@package", 475 new InputSource(new FileInputStream(manifest))); 476 477 mSdkLog.printf("Found main project package: %1$s\n", packageName); 478 479 // now get the name of the first activity we find 480 activityName = xpath.evaluate("/manifest/application/activity[1]/@android:name", 481 new InputSource(new FileInputStream(manifest))); 482 // xpath will return empty string when there's no match 483 if (activityName == null || activityName.length() == 0) { 484 activityName = null; 485 } else { 486 mSdkLog.printf("Found main project activity: %1$s\n", activityName); 487 } 488 } catch (FileNotFoundException e) { 489 // this shouldn't happen as we test it above. 490 errorAndExit("No AndroidManifest.xml file found in main project."); 491 return; // this is not strictly needed because errorAndExit will stop the execution, 492 // but this makes the java compiler happy, wrt to uninitialized variables. 493 } catch (XPathExpressionException e) { 494 // looks like the main manifest is not valid. 495 errorAndExit("Unable to parse main project manifest to get information."); 496 return; // this is not strictly needed because errorAndExit will stop the execution, 497 // but this makes the java compiler happy, wrt to uninitialized variables. 498 } 499 500 // now get the target hash 501 ProjectProperties p = ProjectProperties.load(parentProject.getAbsolutePath(), 502 PropertyType.DEFAULT); 503 if (p == null) { 504 errorAndExit("Unable to load the main project's %1$s", 505 PropertyType.DEFAULT.getFilename()); 506 return; 507 } 508 509 String targetHash = p.getProperty(ProjectProperties.PROPERTY_TARGET); 510 if (targetHash == null) { 511 errorAndExit("Couldn't find the main project target"); 512 return; 513 } 514 515 // and resolve it. 516 IAndroidTarget target = mSdkManager.getTargetFromHashString(targetHash); 517 if (target == null) { 518 errorAndExit( 519 "Unable to resolve main project target '%1$s'. You may want to install the platform in your SDK.", 520 targetHash); 521 return; 522 } 523 524 mSdkLog.printf("Found main project target: %1$s\n", target.getFullName()); 525 526 ProjectCreator creator = getProjectCreator(); 527 528 String projectName = mSdkCommandLine.getParamName(); 529 530 if (projectName != null && 531 !ProjectCreator.RE_PROJECT_NAME.matcher(projectName).matches()) { 532 errorAndExit( 533 "Project name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", 534 projectName, ProjectCreator.CHARS_PROJECT_NAME); 535 return; 536 } 537 538 creator.createProject(projectDir, 539 projectName, 540 packageName, 541 activityName, 542 target, 543 false /* library*/, 544 pathToMainProject); 545 } 546 547 /** 548 * Creates a new Android Export project based on command-line parameters 549 */ createExportProject()550 private void createExportProject() { 551 ProjectCreator creator = getProjectCreator(); 552 553 String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); 554 555 String projectName = mSdkCommandLine.getParamName(); 556 String packageName = mSdkCommandLine.getParamProjectPackage( 557 SdkCommandLine.OBJECT_EXPORT_PROJECT); 558 559 if (projectName != null && 560 !ProjectCreator.RE_PROJECT_NAME.matcher(projectName).matches()) { 561 errorAndExit( 562 "Project name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", 563 projectName, ProjectCreator.CHARS_PROJECT_NAME); 564 return; 565 } 566 567 if (packageName != null && 568 !ProjectCreator.RE_PACKAGE_NAME.matcher(packageName).matches()) { 569 errorAndExit( 570 "Package name '%1$s' contains invalid characters.\n" + 571 "A package name must be constitued of two Java identifiers.\n" + 572 "Each identifier allowed characters are: %2$s", 573 packageName, ProjectCreator.CHARS_PACKAGE_NAME); 574 return; 575 } 576 577 creator.createExportProject(projectDir, projectName, packageName); 578 } 579 580 /** 581 * Updates an existing Android project based on command-line parameters 582 * @param library whether the project is a library project. 583 */ updateProject(boolean library)584 private void updateProject(boolean library) { 585 // get the target and try to resolve it. 586 IAndroidTarget target = null; 587 String targetStr = mSdkCommandLine.getParamTargetId(); 588 // For "update project" the target parameter is optional so having null is acceptable. 589 // However if there's a value, it must be valid. 590 if (targetStr != null) { 591 IAndroidTarget[] targets = mSdkManager.getTargets(); 592 int targetId = resolveTargetName(targetStr); 593 if (targetId == INVALID_TARGET_ID || targetId > targets.length) { 594 errorAndExit("Target id '%1$s' is not valid. Use '%2$s list targets' to get the target ids.", 595 targetStr, 596 SdkConstants.androidCmdName()); 597 } 598 target = targets[targetId - 1]; // target id is 1-based 599 } 600 601 ProjectCreator creator = getProjectCreator(); 602 603 String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); 604 605 String libraryPath = library ? null : 606 mSdkCommandLine.getParamProjectLibrary(SdkCommandLine.OBJECT_PROJECT); 607 608 creator.updateProject(projectDir, 609 target, 610 mSdkCommandLine.getParamName(), 611 libraryPath); 612 613 if (library == false) { 614 boolean doSubProjects = mSdkCommandLine.getParamSubProject(); 615 boolean couldHaveDone = false; 616 617 // If there are any sub-folders with a manifest, try to update them as projects 618 // too. This will take care of updating any underlying test project even if the 619 // user changed the folder name. 620 File[] files = new File(projectDir).listFiles(); 621 if (files != null) { 622 for (File dir : files) { 623 if (dir.isDirectory() && 624 new File(dir, SdkConstants.FN_ANDROID_MANIFEST_XML).isFile()) { 625 if (doSubProjects) { 626 creator.updateProject(dir.getPath(), 627 target, 628 mSdkCommandLine.getParamName(), 629 null /*libraryPath*/); 630 } else { 631 couldHaveDone = true; 632 } 633 } 634 } 635 } 636 637 if (couldHaveDone) { 638 mSdkLog.printf( 639 "It seems that there are sub-projects. If you want to update them\nplease use the --%1$s parameter.\n", 640 SdkCommandLine.KEY_SUBPROJECTS); 641 } 642 } 643 } 644 645 /** 646 * Updates an existing test project with a new path to the main project. 647 */ updateTestProject()648 private void updateTestProject() { 649 ProjectCreator creator = getProjectCreator(); 650 651 String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); 652 653 creator.updateTestProject(projectDir, mSdkCommandLine.getParamTestProjectMain(), 654 mSdkManager); 655 } 656 657 /** 658 * Updates an existing Android export project based on command-line parameters 659 */ updateExportProject()660 private void updateExportProject() { 661 ProjectCreator creator = getProjectCreator(); 662 663 String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); 664 665 String projectName = mSdkCommandLine.getParamName(); 666 if (projectName != null && 667 !ProjectCreator.RE_PROJECT_NAME.matcher(projectName).matches()) { 668 errorAndExit( 669 "Project name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", 670 projectName, ProjectCreator.CHARS_PROJECT_NAME); 671 return; 672 } 673 674 creator.updateExportProject(projectDir, projectName, mSdkCommandLine.getFlagForce()); 675 } 676 677 /** 678 * Adjusts the project location to make it absolute & canonical relative to the 679 * working directory, if any. 680 * 681 * @return The project absolute path relative to {@link #mWorkDir} or the original 682 * newProjectLocation otherwise. 683 */ getProjectLocation(String newProjectLocation)684 private String getProjectLocation(String newProjectLocation) { 685 686 // If the new project location is absolute, use it as-is 687 File projectDir = new File(newProjectLocation); 688 if (projectDir.isAbsolute()) { 689 return newProjectLocation; 690 } 691 692 // if there's no working directory, just use the project location as-is. 693 if (mWorkDir == null) { 694 return newProjectLocation; 695 } 696 697 // Combine then and get an absolute canonical directory 698 try { 699 projectDir = new File(mWorkDir, newProjectLocation).getCanonicalFile(); 700 701 return projectDir.getPath(); 702 } catch (IOException e) { 703 errorAndExit("Failed to combine working directory '%1$s' with project location '%2$s': %3$s", 704 mWorkDir.getPath(), 705 newProjectLocation, 706 e.getMessage()); 707 return null; 708 } 709 } 710 711 /** 712 * Displays the list of available Targets (Platforms and Add-ons) 713 */ displayTargetList()714 private void displayTargetList() { 715 mSdkLog.printf("Available Android targets:\n"); 716 717 int index = 1; 718 for (IAndroidTarget target : mSdkManager.getTargets()) { 719 mSdkLog.printf("id: %1$d or \"%2$s\"\n", index, target.hashString()); 720 mSdkLog.printf(" Name: %s\n", target.getName()); 721 if (target.isPlatform()) { 722 mSdkLog.printf(" Type: Platform\n"); 723 mSdkLog.printf(" API level: %s\n", target.getVersion().getApiString()); 724 mSdkLog.printf(" Revision: %d\n", target.getRevision()); 725 } else { 726 mSdkLog.printf(" Type: Add-On\n"); 727 mSdkLog.printf(" Vendor: %s\n", target.getVendor()); 728 mSdkLog.printf(" Revision: %d\n", target.getRevision()); 729 if (target.getDescription() != null) { 730 mSdkLog.printf(" Description: %s\n", target.getDescription()); 731 } 732 mSdkLog.printf(" Based on Android %s (API level %s)\n", 733 target.getVersionName(), target.getVersion().getApiString()); 734 735 // display the optional libraries. 736 IOptionalLibrary[] libraries = target.getOptionalLibraries(); 737 if (libraries != null) { 738 mSdkLog.printf(" Libraries:\n"); 739 for (IOptionalLibrary library : libraries) { 740 mSdkLog.printf(" * %1$s (%2$s)\n", 741 library.getName(), library.getJarName()); 742 mSdkLog.printf(String.format( 743 " %1$s\n", library.getDescription())); 744 } 745 } 746 } 747 748 // get the target skins 749 displaySkinList(target, " Skins: "); 750 751 if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) { 752 mSdkLog.printf(" Adds USB support for devices (Vendor: 0x%04X)\n", 753 target.getUsbVendorId()); 754 } 755 756 index++; 757 } 758 } 759 760 /** 761 * Displays the skins valid for the given target. 762 */ displaySkinList(IAndroidTarget target, String message)763 private void displaySkinList(IAndroidTarget target, String message) { 764 String[] skins = target.getSkins(); 765 String defaultSkin = target.getDefaultSkin(); 766 mSdkLog.printf(message); 767 if (skins != null) { 768 boolean first = true; 769 for (String skin : skins) { 770 if (first == false) { 771 mSdkLog.printf(", "); 772 } else { 773 first = false; 774 } 775 mSdkLog.printf(skin); 776 777 if (skin.equals(defaultSkin)) { 778 mSdkLog.printf(" (default)"); 779 } 780 } 781 mSdkLog.printf("\n"); 782 } else { 783 mSdkLog.printf("no skins.\n"); 784 } 785 } 786 787 /** 788 * Displays the list of available AVDs. 789 */ displayAvdList()790 private void displayAvdList() { 791 try { 792 AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); 793 794 mSdkLog.printf("Available Android Virtual Devices:\n"); 795 796 AvdInfo[] avds = avdManager.getValidAvds(); 797 for (int index = 0 ; index < avds.length ; index++) { 798 AvdInfo info = avds[index]; 799 if (index > 0) { 800 mSdkLog.printf("---------\n"); 801 } 802 mSdkLog.printf(" Name: %s\n", info.getName()); 803 mSdkLog.printf(" Path: %s\n", info.getPath()); 804 805 // get the target of the AVD 806 IAndroidTarget target = info.getTarget(); 807 if (target.isPlatform()) { 808 mSdkLog.printf(" Target: %s (API level %s)\n", target.getName(), 809 target.getVersion().getApiString()); 810 } else { 811 mSdkLog.printf(" Target: %s (%s)\n", target.getName(), target 812 .getVendor()); 813 mSdkLog.printf(" Based on Android %s (API level %s)\n", 814 target.getVersionName(), target.getVersion().getApiString()); 815 } 816 817 // display some extra values. 818 Map<String, String> properties = info.getProperties(); 819 if (properties != null) { 820 String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); 821 if (skin != null) { 822 mSdkLog.printf(" Skin: %s\n", skin); 823 } 824 String sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); 825 if (sdcard == null) { 826 sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); 827 } 828 if (sdcard != null) { 829 mSdkLog.printf(" Sdcard: %s\n", sdcard); 830 } 831 } 832 } 833 834 // Are there some unused AVDs? 835 AvdInfo[] badAvds = avdManager.getBrokenAvds(); 836 837 if (badAvds.length == 0) { 838 return; 839 } 840 841 mSdkLog.printf("\nThe following Android Virtual Devices could not be loaded:\n"); 842 boolean needSeparator = false; 843 for (AvdInfo info : badAvds) { 844 if (needSeparator) { 845 mSdkLog.printf("---------\n"); 846 } 847 mSdkLog.printf(" Name: %s\n", info.getName() == null ? "--" : info.getName()); 848 mSdkLog.printf(" Path: %s\n", info.getPath() == null ? "--" : info.getPath()); 849 850 String error = info.getErrorMessage(); 851 mSdkLog.printf(" Error: %s\n", error == null ? "Uknown error" : error); 852 needSeparator = true; 853 } 854 } catch (AndroidLocationException e) { 855 errorAndExit(e.getMessage()); 856 } 857 } 858 859 /** 860 * Creates a new AVD. This is a text based creation with command line prompt. 861 */ createAvd()862 private void createAvd() { 863 // find a matching target 864 int targetId = resolveTargetName(mSdkCommandLine.getParamTargetId()); 865 IAndroidTarget[] targets = mSdkManager.getTargets(); 866 867 if (targetId == INVALID_TARGET_ID || targetId > targets.length) { 868 errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", 869 SdkConstants.androidCmdName()); 870 } 871 872 IAndroidTarget target = targets[targetId-1]; // target id is 1-based 873 874 try { 875 boolean removePrevious = mSdkCommandLine.getFlagForce(); 876 AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); 877 878 String avdName = mSdkCommandLine.getParamName(); 879 880 if (!AvdManager.RE_AVD_NAME.matcher(avdName).matches()) { 881 errorAndExit( 882 "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", 883 avdName, AvdManager.CHARS_AVD_NAME); 884 return; 885 } 886 887 AvdInfo info = avdManager.getAvd(avdName, false /*validAvdOnly*/); 888 if (info != null) { 889 if (removePrevious) { 890 mSdkLog.warning( 891 "Android Virtual Device '%s' already exists and will be replaced.", 892 avdName); 893 } else { 894 errorAndExit("Android Virtual Device '%s' already exists.\n" + 895 "Use --force if you want to replace it.", 896 avdName); 897 return; 898 } 899 } 900 901 String paramFolderPath = mSdkCommandLine.getParamLocationPath(); 902 File avdFolder = null; 903 if (paramFolderPath != null) { 904 avdFolder = new File(paramFolderPath); 905 } else { 906 avdFolder = new File(AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, 907 avdName + AvdManager.AVD_FOLDER_EXTENSION); 908 } 909 910 // Validate skin is either default (empty) or NNNxMMM or a valid skin name. 911 Map<String, String> skinHardwareConfig = null; 912 String skin = mSdkCommandLine.getParamSkin(); 913 if (skin != null && skin.length() == 0) { 914 skin = null; 915 } 916 917 if (skin != null && target != null) { 918 boolean valid = false; 919 // Is it a know skin name for this target? 920 for (String s : target.getSkins()) { 921 if (skin.equalsIgnoreCase(s)) { 922 skin = s; // Make skin names case-insensitive. 923 valid = true; 924 925 // get the hardware properties for this skin 926 File skinFolder = avdManager.getSkinPath(skin, target); 927 File skinHardwareFile = new File(skinFolder, AvdManager.HARDWARE_INI); 928 if (skinHardwareFile.isFile()) { 929 skinHardwareConfig = ProjectProperties.parsePropertyFile( 930 skinHardwareFile, mSdkLog); 931 } 932 break; 933 } 934 } 935 936 // Is it NNNxMMM? 937 if (!valid) { 938 valid = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin).matches(); 939 } 940 941 if (!valid) { 942 displaySkinList(target, "Valid skins: "); 943 errorAndExit("'%s' is not a valid skin name or size (NNNxMMM)", skin); 944 return; 945 } 946 } 947 948 Map<String, String> hardwareConfig = null; 949 if (target != null && target.isPlatform()) { 950 try { 951 hardwareConfig = promptForHardware(target, skinHardwareConfig); 952 } catch (IOException e) { 953 errorAndExit(e.getMessage()); 954 } 955 } 956 957 @SuppressWarnings("unused") // oldAvdInfo is never read, yet useful for debugging 958 AvdInfo oldAvdInfo = null; 959 if (removePrevious) { 960 oldAvdInfo = avdManager.getAvd(avdName, false /*validAvdOnly*/); 961 } 962 963 @SuppressWarnings("unused") // newAvdInfo is never read, yet useful for debugging 964 AvdInfo newAvdInfo = avdManager.createAvd(avdFolder, 965 avdName, 966 target, 967 skin, 968 mSdkCommandLine.getParamSdCard(), 969 hardwareConfig, 970 removePrevious, 971 mSdkLog); 972 973 } catch (AndroidLocationException e) { 974 errorAndExit(e.getMessage()); 975 } 976 } 977 978 /** 979 * Delete an AVD. If the AVD name is not part of the available ones look for an 980 * invalid AVD (one not loaded due to some error) to remove it too. 981 */ deleteAvd()982 private void deleteAvd() { 983 try { 984 String avdName = mSdkCommandLine.getParamName(); 985 AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); 986 AvdInfo info = avdManager.getAvd(avdName, false /*validAvdOnly*/); 987 988 if (info == null) { 989 errorAndExit("There is no Android Virtual Device named '%s'.", avdName); 990 return; 991 } 992 993 avdManager.deleteAvd(info, mSdkLog); 994 } catch (AndroidLocationException e) { 995 errorAndExit(e.getMessage()); 996 } 997 } 998 999 /** 1000 * Moves an AVD. 1001 */ moveAvd()1002 private void moveAvd() { 1003 try { 1004 String avdName = mSdkCommandLine.getParamName(); 1005 AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); 1006 AvdInfo info = avdManager.getAvd(avdName, true /*validAvdOnly*/); 1007 1008 if (info == null) { 1009 errorAndExit("There is no valid Android Virtual Device named '%s'.", avdName); 1010 return; 1011 } 1012 1013 // This is a rename if there's a new name for the AVD 1014 String newName = mSdkCommandLine.getParamMoveNewName(); 1015 if (newName != null && newName.equals(info.getName())) { 1016 // same name, not actually a rename operation 1017 newName = null; 1018 } 1019 1020 // This is a move (of the data files) if there's a new location path 1021 String paramFolderPath = mSdkCommandLine.getParamLocationPath(); 1022 if (paramFolderPath != null) { 1023 // check if paths are the same. Use File methods to account for OS idiosyncrasies. 1024 try { 1025 File f1 = new File(paramFolderPath).getCanonicalFile(); 1026 File f2 = new File(info.getPath()).getCanonicalFile(); 1027 if (f1.equals(f2)) { 1028 // same canonical path, so not actually a move 1029 paramFolderPath = null; 1030 } 1031 } catch (IOException e) { 1032 // Fail to resolve canonical path. Fail now since a move operation might fail 1033 // later and be harder to recover from. 1034 errorAndExit(e.getMessage()); 1035 return; 1036 } 1037 } 1038 1039 if (newName == null && paramFolderPath == null) { 1040 mSdkLog.warning("Move operation aborted: same AVD name, same canonical data path"); 1041 return; 1042 } 1043 1044 // If a rename was requested and no data move was requested, check if the original 1045 // data path is our default constructed from the AVD name. In this case we still want 1046 // to rename that folder too. 1047 if (newName != null && paramFolderPath == null) { 1048 // Compute the original data path 1049 File originalFolder = new File( 1050 AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, 1051 info.getName() + AvdManager.AVD_FOLDER_EXTENSION); 1052 if (originalFolder.equals(info.getPath())) { 1053 try { 1054 // The AVD is using the default data folder path based on the AVD name. 1055 // That folder needs to be adjusted to use the new name. 1056 File f = new File(AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, 1057 newName + AvdManager.AVD_FOLDER_EXTENSION); 1058 paramFolderPath = f.getCanonicalPath(); 1059 } catch (IOException e) { 1060 // Fail to resolve canonical path. Fail now rather than later. 1061 errorAndExit(e.getMessage()); 1062 } 1063 } 1064 } 1065 1066 // Check for conflicts 1067 if (newName != null) { 1068 if (avdManager.getAvd(newName, false /*validAvdOnly*/) != null) { 1069 errorAndExit("There is already an AVD named '%s'.", newName); 1070 return; 1071 } 1072 1073 File ini = info.getIniFile(); 1074 if (ini.equals(AvdInfo.getIniFile(newName))) { 1075 errorAndExit("The AVD file '%s' is in the way.", ini.getCanonicalPath()); 1076 return; 1077 } 1078 } 1079 1080 if (paramFolderPath != null && new File(paramFolderPath).exists()) { 1081 errorAndExit( 1082 "There is already a file or directory at '%s'.\nUse --path to specify a different data folder.", 1083 paramFolderPath); 1084 } 1085 1086 avdManager.moveAvd(info, newName, paramFolderPath, mSdkLog); 1087 } catch (AndroidLocationException e) { 1088 errorAndExit(e.getMessage()); 1089 } catch (IOException e) { 1090 errorAndExit(e.getMessage()); 1091 } 1092 } 1093 1094 /** 1095 * Updates a broken AVD. 1096 */ updateAvd()1097 private void updateAvd() { 1098 try { 1099 String avdName = mSdkCommandLine.getParamName(); 1100 AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); 1101 avdManager.updateAvd(avdName, mSdkLog); 1102 } catch (AndroidLocationException e) { 1103 errorAndExit(e.getMessage()); 1104 } catch (IOException e) { 1105 errorAndExit(e.getMessage()); 1106 } 1107 } 1108 1109 /** 1110 * Updates adb with the USB devices declared in the SDK add-ons. 1111 */ updateAdb()1112 private void updateAdb() { 1113 try { 1114 mSdkManager.updateAdb(); 1115 1116 mSdkLog.printf( 1117 "adb has been updated. You must restart adb with the following commands\n" + 1118 "\tadb kill-server\n" + 1119 "\tadb start-server\n"); 1120 } catch (AndroidLocationException e) { 1121 errorAndExit(e.getMessage()); 1122 } catch (IOException e) { 1123 errorAndExit(e.getMessage()); 1124 } 1125 } 1126 1127 /** 1128 * Prompts the user to setup a hardware config for a Platform-based AVD. 1129 * @throws IOException 1130 */ promptForHardware(IAndroidTarget createTarget, Map<String, String> skinHardwareConfig)1131 private Map<String, String> promptForHardware(IAndroidTarget createTarget, 1132 Map<String, String> skinHardwareConfig) throws IOException { 1133 byte[] readLineBuffer = new byte[256]; 1134 String result; 1135 String defaultAnswer = "no"; 1136 1137 mSdkLog.printf("%s is a basic Android platform.\n", createTarget.getName()); 1138 mSdkLog.printf("Do you wish to create a custom hardware profile [%s]", 1139 defaultAnswer); 1140 1141 result = readLine(readLineBuffer).trim(); 1142 // handle default: 1143 if (result.length() == 0) { 1144 result = defaultAnswer; 1145 } 1146 1147 if (getBooleanReply(result) == false) { 1148 // no custom config, return the skin hardware config in case there is one. 1149 return skinHardwareConfig; 1150 } 1151 1152 mSdkLog.printf("\n"); // empty line 1153 1154 // get the list of possible hardware properties 1155 File hardwareDefs = new File (mOsSdkFolder + File.separator + 1156 SdkConstants.OS_SDK_TOOLS_LIB_FOLDER, SdkConstants.FN_HARDWARE_INI); 1157 Map<String, HardwareProperty> hwMap = HardwareProperties.parseHardwareDefinitions( 1158 hardwareDefs, null /*sdkLog*/); 1159 1160 HashMap<String, String> map = new HashMap<String, String>(); 1161 1162 // we just want to loop on the HardwareProperties 1163 HardwareProperty[] hwProperties = hwMap.values().toArray( 1164 new HardwareProperty[hwMap.size()]); 1165 for (int i = 0 ; i < hwProperties.length ;) { 1166 HardwareProperty property = hwProperties[i]; 1167 1168 String description = property.getDescription(); 1169 if (description != null) { 1170 mSdkLog.printf("%s: %s\n", property.getAbstract(), description); 1171 } else { 1172 mSdkLog.printf("%s\n", property.getAbstract()); 1173 } 1174 1175 String defaultValue = property.getDefault(); 1176 String defaultFromSkin = skinHardwareConfig != null ? skinHardwareConfig.get( 1177 property.getName()) : null; 1178 1179 if (defaultFromSkin != null) { 1180 mSdkLog.printf("%s [%s (from skin)]:", property.getName(), defaultFromSkin); 1181 } else if (defaultValue != null) { 1182 mSdkLog.printf("%s [%s]:", property.getName(), defaultValue); 1183 } else { 1184 mSdkLog.printf("%s (%s):", property.getName(), property.getType()); 1185 } 1186 1187 result = readLine(readLineBuffer); 1188 if (result.length() == 0) { 1189 if (defaultFromSkin != null || defaultValue != null) { 1190 if (defaultFromSkin != null) { 1191 // we need to write this one in the AVD file 1192 map.put(property.getName(), defaultFromSkin); 1193 } 1194 1195 mSdkLog.printf("\n"); // empty line 1196 i++; // go to the next property if we have a valid default value. 1197 // if there's no default, we'll redo this property 1198 } 1199 continue; 1200 } 1201 1202 switch (property.getType()) { 1203 case BOOLEAN: 1204 try { 1205 if (getBooleanReply(result)) { 1206 map.put(property.getName(), "yes"); 1207 i++; // valid reply, move to next property 1208 } else { 1209 map.put(property.getName(), "no"); 1210 i++; // valid reply, move to next property 1211 } 1212 } catch (IOException e) { 1213 // display error, and do not increment i to redo this property 1214 mSdkLog.printf("\n%s\n", e.getMessage()); 1215 } 1216 break; 1217 case INTEGER: 1218 try { 1219 Integer.parseInt(result); 1220 map.put(property.getName(), result); 1221 i++; // valid reply, move to next property 1222 } catch (NumberFormatException e) { 1223 // display error, and do not increment i to redo this property 1224 mSdkLog.printf("\n%s\n", e.getMessage()); 1225 } 1226 break; 1227 case DISKSIZE: 1228 // TODO check validity 1229 map.put(property.getName(), result); 1230 i++; // valid reply, move to next property 1231 break; 1232 } 1233 1234 mSdkLog.printf("\n"); // empty line 1235 } 1236 1237 return map; 1238 } 1239 1240 /** 1241 * Reads the line from the input stream. 1242 * @param buffer 1243 * @throws IOException 1244 */ readLine(byte[] buffer)1245 private String readLine(byte[] buffer) throws IOException { 1246 int count = System.in.read(buffer); 1247 1248 // is the input longer than the buffer? 1249 if (count == buffer.length && buffer[count-1] != 10) { 1250 // create a new temp buffer 1251 byte[] tempBuffer = new byte[256]; 1252 1253 // and read the rest 1254 String secondHalf = readLine(tempBuffer); 1255 1256 // return a concat of both 1257 return new String(buffer, 0, count) + secondHalf; 1258 } 1259 1260 // ignore end whitespace 1261 while (count > 0 && (buffer[count-1] == '\r' || buffer[count-1] == '\n')) { 1262 count--; 1263 } 1264 1265 return new String(buffer, 0, count); 1266 } 1267 1268 /** 1269 * Returns the boolean value represented by the string. 1270 * @throws IOException If the value is not a boolean string. 1271 */ getBooleanReply(String reply)1272 private boolean getBooleanReply(String reply) throws IOException { 1273 1274 for (String valid : BOOLEAN_YES_REPLIES) { 1275 if (valid.equalsIgnoreCase(reply)) { 1276 return true; 1277 } 1278 } 1279 1280 for (String valid : BOOLEAN_NO_REPLIES) { 1281 if (valid.equalsIgnoreCase(reply)) { 1282 return false; 1283 } 1284 } 1285 1286 throw new IOException(String.format("%s is not a valid reply", reply)); 1287 } 1288 errorAndExit(String format, Object...args)1289 private void errorAndExit(String format, Object...args) { 1290 mSdkLog.error(null, format, args); 1291 System.exit(1); 1292 } 1293 1294 /** 1295 * Converts a symbolic target name (such as those accepted by --target on the command-line) 1296 * to an internal target index id. A valid target name is either a numeric target id (> 0) 1297 * or a target hash string. 1298 * <p/> 1299 * If the given target can't be mapped, {@link #INVALID_TARGET_ID} (0) is returned. 1300 * It's up to the caller to output an error. 1301 * <p/> 1302 * On success, returns a value > 0. 1303 */ resolveTargetName(String targetName)1304 private int resolveTargetName(String targetName) { 1305 1306 if (targetName == null) { 1307 return INVALID_TARGET_ID; 1308 } 1309 1310 targetName = targetName.trim(); 1311 1312 // Case of an integer number 1313 if (targetName.matches("[0-9]*")) { 1314 try { 1315 int n = Integer.parseInt(targetName); 1316 return n < 1 ? INVALID_TARGET_ID : n; 1317 } catch (NumberFormatException e) { 1318 // Ignore. Should not happen. 1319 } 1320 } 1321 1322 // Let's try to find a platform or addon name. 1323 IAndroidTarget[] targets = mSdkManager.getTargets(); 1324 for (int i = 0; i < targets.length; i++) { 1325 if (targetName.equals(targets[i].hashString())) { 1326 return i + 1; 1327 } 1328 } 1329 1330 return INVALID_TARGET_ID; 1331 } 1332 } 1333