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