• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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