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