• 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.sdklib.internal.avd;
18 
19 import com.android.io.FileWrapper;
20 import com.android.prefs.AndroidLocation;
21 import com.android.prefs.AndroidLocation.AndroidLocationException;
22 import com.android.sdklib.IAndroidTarget;
23 import com.android.sdklib.ISdkLog;
24 import com.android.sdklib.ISystemImage;
25 import com.android.sdklib.SdkConstants;
26 import com.android.sdklib.SdkManager;
27 import com.android.sdklib.internal.avd.AvdInfo.AvdStatus;
28 import com.android.sdklib.internal.project.ProjectProperties;
29 import com.android.util.Pair;
30 
31 import java.io.BufferedReader;
32 import java.io.File;
33 import java.io.FileInputStream;
34 import java.io.FileNotFoundException;
35 import java.io.FileOutputStream;
36 import java.io.FileWriter;
37 import java.io.FilenameFilter;
38 import java.io.IOException;
39 import java.io.InputStreamReader;
40 import java.io.OutputStreamWriter;
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.Map;
44 import java.util.Map.Entry;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 
48 /**
49  * Android Virtual Device Manager to manage AVDs.
50  */
51 public class AvdManager {
52 
53     /**
54      * Exception thrown when something is wrong with a target path.
55      */
56     private final static class InvalidTargetPathException extends Exception {
57         private static final long serialVersionUID = 1L;
58 
InvalidTargetPathException(String message)59         InvalidTargetPathException(String message) {
60             super(message);
61         }
62     }
63 
64     public static final String AVD_FOLDER_EXTENSION = ".avd";  //$NON-NLS-1$
65 
66     public final static String AVD_INFO_PATH = "path";         //$NON-NLS-1$
67     public final static String AVD_INFO_TARGET = "target";     //$NON-NLS-1$
68 
69     /**
70      * AVD/config.ini key name representing the abi type of the specific avd
71      *
72      */
73     public final static String AVD_INI_ABI_TYPE = "abi.type"; //$NON-NLS-1$
74 
75     /**
76      * AVD/config.ini key name representing the CPU architecture of the specific avd
77      *
78      */
79     public final static String AVD_INI_CPU_ARCH = "hw.cpu.arch"; //$NON-NLS-1$
80 
81     /**
82      * AVD/config.ini key name representing the CPU architecture of the specific avd
83      *
84      */
85     public final static String AVD_INI_CPU_MODEL = "hw.cpu.model"; //$NON-NLS-1$
86 
87 
88     /**
89      * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any,
90      * or a 320x480 like constant for a numeric skin size.
91      *
92      * @see #NUMERIC_SKIN_SIZE
93      */
94     public final static String AVD_INI_SKIN_PATH = "skin.path"; //$NON-NLS-1$
95     /**
96      * AVD/config.ini key name representing an UI name for the skin.
97      * This config key is ignored by the emulator. It is only used by the SDK manager or
98      * tools to give a friendlier name to the skin.
99      * If missing, use the {@link #AVD_INI_SKIN_PATH} key instead.
100      */
101     public final static String AVD_INI_SKIN_NAME = "skin.name"; //$NON-NLS-1$
102     /**
103      * AVD/config.ini key name representing the path to the sdcard file.
104      * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such
105      * a file.
106      *
107      * @see #SDCARD_IMG
108      */
109     public final static String AVD_INI_SDCARD_PATH = "sdcard.path"; //$NON-NLS-1$
110     /**
111      * AVD/config.ini key name representing the size of the SD card.
112      * This property is for UI purposes only. It is not used by the emulator.
113      *
114      * @see #SDCARD_SIZE_PATTERN
115      * @see #parseSdcardSize(String, String[])
116      */
117     public final static String AVD_INI_SDCARD_SIZE = "sdcard.size"; //$NON-NLS-1$
118     /**
119      * AVD/config.ini key name representing the first path where the emulator looks
120      * for system images. Typically this is the path to the add-on system image or
121      * the path to the platform system image if there's no add-on.
122      * <p/>
123      * The emulator looks at {@link #AVD_INI_IMAGES_1} before {@link #AVD_INI_IMAGES_2}.
124      */
125     public final static String AVD_INI_IMAGES_1 = "image.sysdir.1"; //$NON-NLS-1$
126     /**
127      * AVD/config.ini key name representing the second path where the emulator looks
128      * for system images. Typically this is the path to the platform system image.
129      *
130      * @see #AVD_INI_IMAGES_1
131      */
132     public final static String AVD_INI_IMAGES_2 = "image.sysdir.2"; //$NON-NLS-1$
133     /**
134      * AVD/config.ini key name representing the presence of the snapshots file.
135      * This property is for UI purposes only. It is not used by the emulator.
136      *
137      * @see #SNAPSHOTS_IMG
138      */
139     public final static String AVD_INI_SNAPSHOT_PRESENT = "snapshot.present"; //$NON-NLS-1$
140 
141     /**
142      * Pattern to match pixel-sized skin "names", e.g. "320x480".
143      */
144     public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$
145 
146     private final static String USERDATA_IMG = "userdata.img"; //$NON-NLS-1$
147     final static String CONFIG_INI = "config.ini"; //$NON-NLS-1$
148     private final static String SDCARD_IMG = "sdcard.img"; //$NON-NLS-1$
149     private final static String SNAPSHOTS_IMG = "snapshots.img"; //$NON-NLS-1$
150 
151     final static String INI_EXTENSION = ".ini"; //$NON-NLS-1$
152     private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + //$NON-NLS-1$
153             INI_EXTENSION + "$",                                               //$NON-NLS-1$
154             Pattern.CASE_INSENSITIVE);
155 
156     private final static Pattern IMAGE_NAME_PATTERN = Pattern.compile("(.+)\\.img$", //$NON-NLS-1$
157             Pattern.CASE_INSENSITIVE);
158 
159     /**
160      * Pattern for matching SD Card sizes, e.g. "4K" or "16M".
161      * Callers should use {@link #parseSdcardSize(String, String[])} instead of using this directly.
162      */
163     private final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("(\\d+)([KMG])"); //$NON-NLS-1$
164 
165     /**
166      * Minimal size of an SDCard image file in bytes. Currently 9 MiB.
167      */
168 
169     public static final long SDCARD_MIN_BYTE_SIZE = 9<<20;
170     /**
171      * Maximal size of an SDCard image file in bytes. Currently 1023 GiB.
172      */
173     public static final long SDCARD_MAX_BYTE_SIZE = 1023L<<30;
174 
175     /** The sdcard string represents a valid number but the size is outside of the allowed range. */
176     public final static int SDCARD_SIZE_NOT_IN_RANGE = 0;
177     /** The sdcard string looks like a size number+suffix but the number failed to decode. */
178     public final static int SDCARD_SIZE_INVALID = -1;
179     /** The sdcard string doesn't look like a size, it might be a path instead. */
180     public final static int SDCARD_NOT_SIZE_PATTERN = -2;
181 
182     /** Regex used to validate characters that compose an AVD name. */
183     public final static Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); //$NON-NLS-1$
184 
185     /** List of valid characters for an AVD name. Used for display purposes. */
186     public final static String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; //$NON-NLS-1$
187 
188     public final static String HARDWARE_INI = "hardware.ini"; //$NON-NLS-1$
189 
190     /**
191      * Status returned by {@link AvdManager#isAvdNameConflicting(String)}.
192      */
193     public static enum AvdConflict {
194         /** There is no known conflict for the given AVD name. */
195         NO_CONFLICT,
196         /** The AVD name conflicts with an existing valid AVD. */
197         CONFLICT_EXISTING_AVD,
198         /** The AVD name conflicts with an existing invalid AVD. */
199         CONFLICT_INVALID_AVD,
200         /**
201          * The AVD name does not conflict with any known AVD however there are
202          * files or directory that would cause a conflict if this were to be created.
203          */
204         CONFLICT_EXISTING_PATH,
205     }
206 
207     private final ArrayList<AvdInfo> mAllAvdList = new ArrayList<AvdInfo>();
208     private AvdInfo[] mValidAvdList;
209     private AvdInfo[] mBrokenAvdList;
210     private final SdkManager mSdkManager;
211 
212     /**
213      * Creates an AVD Manager for a given SDK represented by a {@link SdkManager}.
214      * @param sdkManager The SDK.
215      * @param log The log object to receive the log of the initial loading of the AVDs.
216      *            This log object is not kept by this instance of AvdManager and each
217      *            method takes its own logger. The rationale is that the AvdManager
218      *            might be called from a variety of context, each with different
219      *            logging needs. Cannot be null.
220      * @throws AndroidLocationException
221      */
AvdManager(SdkManager sdkManager, ISdkLog log)222     public AvdManager(SdkManager sdkManager, ISdkLog log) throws AndroidLocationException {
223         mSdkManager = sdkManager;
224         buildAvdList(mAllAvdList, log);
225     }
226 
227     /**
228      * Returns the base folder where AVDs are created.
229      *
230      * @throws AndroidLocationException
231      */
getBaseAvdFolder()232     public String getBaseAvdFolder() throws AndroidLocationException {
233         assert AndroidLocation.getFolder().endsWith(File.separator);
234         return AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD;
235     }
236 
237     /**
238      * Returns the {@link SdkManager} associated with the {@link AvdManager}.
239      */
getSdkManager()240     public SdkManager getSdkManager() {
241         return mSdkManager;
242     }
243 
244     /**
245      * Parse the sdcard string to decode the size.
246      * Returns:
247      * <ul>
248      * <li> The size in bytes > 0 if the sdcard string is a valid size in the allowed range.
249      * <li> {@link #SDCARD_SIZE_NOT_IN_RANGE} (0)
250      *          if the sdcard string is a valid size NOT in the allowed range.
251      * <li> {@link #SDCARD_SIZE_INVALID} (-1)
252      *          if the sdcard string is number that fails to parse correctly.
253      * <li> {@link #SDCARD_NOT_SIZE_PATTERN} (-2)
254      *          if the sdcard string is not a number, in which case it's probably a file path.
255      * </ul>
256      *
257      * @param sdcard The sdcard string, which can be a file path, a size string or something else.
258      * @param parsedStrings If non-null, an array of 2 strings. The first string will be
259      *  filled with the parsed numeric size and the second one will be filled with the
260      *  parsed suffix. This is filled even if the returned size is deemed out of range or
261      *  failed to parse. The values are null if the sdcard is not a size pattern.
262      * @return A size in byte if > 0, or {@link #SDCARD_SIZE_NOT_IN_RANGE},
263      *  {@link #SDCARD_SIZE_INVALID} or {@link #SDCARD_NOT_SIZE_PATTERN} as error codes.
264      */
parseSdcardSize(String sdcard, String[] parsedStrings)265     public static long parseSdcardSize(String sdcard, String[] parsedStrings) {
266 
267         if (parsedStrings != null) {
268             assert parsedStrings.length == 2;
269             parsedStrings[0] = null;
270             parsedStrings[1] = null;
271         }
272 
273         Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard);
274         if (m.matches()) {
275             if (parsedStrings != null) {
276                 assert parsedStrings.length == 2;
277                 parsedStrings[0] = m.group(1);
278                 parsedStrings[1] = m.group(2);
279             }
280 
281             // get the sdcard values for checks
282             try {
283                 long sdcardSize = Long.parseLong(m.group(1));
284 
285                 String sdcardSizeModifier = m.group(2);
286                 if ("K".equals(sdcardSizeModifier)) {           //$NON-NLS-1$
287                     sdcardSize <<= 10;
288                 } else if ("M".equals(sdcardSizeModifier)) {    //$NON-NLS-1$
289                     sdcardSize <<= 20;
290                 } else if ("G".equals(sdcardSizeModifier)) {    //$NON-NLS-1$
291                     sdcardSize <<= 30;
292                 }
293 
294                 if (sdcardSize < SDCARD_MIN_BYTE_SIZE ||
295                         sdcardSize > SDCARD_MAX_BYTE_SIZE) {
296                     return SDCARD_SIZE_NOT_IN_RANGE;
297                 }
298 
299                 return sdcardSize;
300             } catch (NumberFormatException e) {
301                 // This could happen if the number is too large to fit in a long.
302                 return SDCARD_SIZE_INVALID;
303             }
304         }
305 
306         return SDCARD_NOT_SIZE_PATTERN;
307     }
308 
309     /**
310      * Returns all the existing AVDs.
311      * @return a newly allocated array containing all the AVDs.
312      */
getAllAvds()313     public AvdInfo[] getAllAvds() {
314         synchronized (mAllAvdList) {
315             return mAllAvdList.toArray(new AvdInfo[mAllAvdList.size()]);
316         }
317     }
318 
319     /**
320      * Returns all the valid AVDs.
321      * @return a newly allocated array containing all valid the AVDs.
322      */
getValidAvds()323     public AvdInfo[] getValidAvds() {
324         synchronized (mAllAvdList) {
325             if (mValidAvdList == null) {
326                 ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
327                 for (AvdInfo avd : mAllAvdList) {
328                     if (avd.getStatus() == AvdStatus.OK) {
329                         list.add(avd);
330                     }
331                 }
332 
333                 mValidAvdList = list.toArray(new AvdInfo[list.size()]);
334             }
335             return mValidAvdList;
336         }
337     }
338 
339     /**
340      * Returns all the broken AVDs.
341      * @return a newly allocated array containing all the broken AVDs.
342      */
getBrokenAvds()343     public AvdInfo[] getBrokenAvds() {
344         synchronized (mAllAvdList) {
345             if (mBrokenAvdList == null) {
346                 ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
347                 for (AvdInfo avd : mAllAvdList) {
348                     if (avd.getStatus() != AvdStatus.OK) {
349                         list.add(avd);
350                     }
351                 }
352                 mBrokenAvdList = list.toArray(new AvdInfo[list.size()]);
353             }
354             return mBrokenAvdList;
355         }
356     }
357 
358     /**
359      * Returns the {@link AvdInfo} matching the given <var>name</var>.
360      * <p/>
361      * The search is case-insensitive.
362      *
363      * @param name the name of the AVD to return
364      * @param validAvdOnly if <code>true</code>, only look through the list of valid AVDs.
365      * @return the matching AvdInfo or <code>null</code> if none were found.
366      */
getAvd(String name, boolean validAvdOnly)367     public AvdInfo getAvd(String name, boolean validAvdOnly) {
368 
369         boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS;
370 
371         if (validAvdOnly) {
372             for (AvdInfo info : getValidAvds()) {
373                 String name2 = info.getName();
374                 if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
375                     return info;
376                 }
377             }
378         } else {
379             synchronized (mAllAvdList) {
380                 for (AvdInfo info : mAllAvdList) {
381                     String name2 = info.getName();
382                     if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
383                         return info;
384                     }
385                 }
386             }
387         }
388 
389         return null;
390     }
391 
392     /**
393      * Returns whether this AVD name would generate a conflict.
394      *
395      * @param name the name of the AVD to return
396      * @return A pair of {@link AvdConflict} and the path or AVD name that conflicts.
397      */
isAvdNameConflicting(String name)398     public Pair<AvdConflict, String> isAvdNameConflicting(String name) {
399 
400         boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS;
401 
402         // Check whether we have a conflict with an existing or invalid AVD
403         // known to the manager.
404         synchronized (mAllAvdList) {
405             for (AvdInfo info : mAllAvdList) {
406                 String name2 = info.getName();
407                 if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
408                     if (info.getStatus() == AvdStatus.OK) {
409                         return Pair.of(AvdConflict.CONFLICT_EXISTING_AVD, name2);
410                     } else {
411                         return Pair.of(AvdConflict.CONFLICT_INVALID_AVD, name2);
412                     }
413                 }
414             }
415         }
416 
417         // No conflict with known AVDs.
418         // Are some existing files/folders in the way of creating this AVD?
419 
420         try {
421             File file = AvdInfo.getDefaultIniFile(this, name);
422             if (file.exists()) {
423                 return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath());
424             }
425 
426             file = AvdInfo.getDefaultAvdFolder(this, name);
427             if (file.exists()) {
428                 return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath());
429             }
430 
431         } catch (AndroidLocationException e) {
432             // ignore
433         }
434 
435 
436         return Pair.of(AvdConflict.NO_CONFLICT, null);
437     }
438 
439     /**
440      * Reloads the AVD list.
441      * @param log the log object to receive action logs. Cannot be null.
442      * @throws AndroidLocationException if there was an error finding the location of the
443      * AVD folder.
444      */
reloadAvds(ISdkLog log)445     public void reloadAvds(ISdkLog log) throws AndroidLocationException {
446         // build the list in a temp list first, in case the method throws an exception.
447         // It's better than deleting the whole list before reading the new one.
448         ArrayList<AvdInfo> allList = new ArrayList<AvdInfo>();
449         buildAvdList(allList, log);
450 
451         synchronized (mAllAvdList) {
452             mAllAvdList.clear();
453             mAllAvdList.addAll(allList);
454             mValidAvdList = mBrokenAvdList = null;
455         }
456     }
457 
458     /**
459      * Creates a new AVD. It is expected that there is no existing AVD with this name already.
460      *
461      * @param avdFolder the data folder for the AVD. It will be created as needed.
462      *   Unless you want to locate it in a specific directory, the ideal default is
463      *   {@code AvdManager.AvdInfo.getAvdFolder}.
464      * @param avdName the name of the AVD
465      * @param target the target of the AVD
466      * @param abiType the abi type of the AVD
467      * @param skinName the name of the skin. Can be null. Must have been verified by caller.
468      * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to
469      *        an existing sdcard image or a sdcard size (\d+, \d+K, \dM).
470      * @param hardwareConfig the hardware setup for the AVD. Can be null to use defaults.
471      * @param createSnapshot If true copy a blank snapshot image into the AVD.
472      * @param removePrevious If true remove any previous files.
473      * @param editExisting If true, edit an existing AVD, changing only the minimum required.
474      *          This won't remove files unless required or unless {@code removePrevious} is set.
475      * @param log the log object to receive action logs. Cannot be null.
476      * @return The new {@link AvdInfo} in case of success (which has just been added to the
477      *         internal list) or null in case of failure.
478      */
createAvd( File avdFolder, String avdName, IAndroidTarget target, String abiType, String skinName, String sdcard, Map<String,String> hardwareConfig, boolean createSnapshot, boolean removePrevious, boolean editExisting, ISdkLog log)479     public AvdInfo createAvd(
480             File avdFolder,
481             String avdName,
482             IAndroidTarget target,
483             String abiType,
484             String skinName,
485             String sdcard,
486             Map<String,String> hardwareConfig,
487             boolean createSnapshot,
488             boolean removePrevious,
489             boolean editExisting,
490             ISdkLog log) {
491         if (log == null) {
492             throw new IllegalArgumentException("log cannot be null");
493         }
494 
495         File iniFile = null;
496         boolean needCleanup = false;
497         try {
498             if (avdFolder.exists()) {
499                 if (removePrevious) {
500                     // AVD already exists and removePrevious is set, try to remove the
501                     // directory's content first (but not the directory itself).
502                     try {
503                         deleteContentOf(avdFolder);
504                     } catch (SecurityException e) {
505                         log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath());
506                     }
507                 } else if (!editExisting) {
508                     // AVD shouldn't already exist if removePrevious is false and
509                     // we're not editing an existing AVD.
510                     log.error(null,
511                             "Folder %1$s is in the way. Use --force if you want to overwrite.",
512                             avdFolder.getAbsolutePath());
513                     return null;
514                 }
515             } else {
516                 // create the AVD folder.
517                 avdFolder.mkdir();
518                 // We're not editing an existing AVD.
519                 editExisting = false;
520             }
521 
522             // actually write the ini file
523             iniFile = createAvdIniFile(avdName, avdFolder, target, removePrevious);
524 
525             // writes the userdata.img in it.
526 
527             File userdataSrc = null;
528 
529             // Look for a system image in the add-on.
530             // If we don't find one there, look in the base platform.
531             ISystemImage systemImage = target.getSystemImage(abiType);
532 
533             if (systemImage != null) {
534                 File imageFolder = systemImage.getLocation();
535                 userdataSrc = new File(imageFolder, USERDATA_IMG);
536             }
537 
538             if ((userdataSrc == null || !userdataSrc.exists()) && !target.isPlatform()) {
539                 // If we don't find a system-image in the add-on, look into the platform.
540 
541                 systemImage = target.getParent().getSystemImage(abiType);
542                 if (systemImage != null) {
543                     File imageFolder = systemImage.getLocation();
544                     userdataSrc = new File(imageFolder, USERDATA_IMG);
545                 }
546             }
547 
548             if (userdataSrc == null || !userdataSrc.exists()) {
549                 log.error(null,
550                         "Unable to find a '%1$s' file for ABI %2$s to copy into the AVD folder.",
551                         USERDATA_IMG,
552                         abiType);
553                 needCleanup = true;
554                 return null;
555             }
556 
557             File userdataDest = new File(avdFolder, USERDATA_IMG);
558 
559             copyImageFile(userdataSrc, userdataDest);
560 
561             if (userdataDest.exists() == false) {
562                 log.error(null, "Unable to create '%1$s' file in the AVD folder.",
563                         userdataDest);
564                 needCleanup = true;
565                 return null;
566             }
567 
568             // Config file.
569             HashMap<String, String> values = new HashMap<String, String>();
570 
571            if (setImagePathProperties(target, abiType, values, log) == false) {
572                log.error(null, "Failed to set image path properties in the AVD folder.");
573                needCleanup = true;
574                return null;
575             }
576 
577             // Create the snapshot file
578             if (createSnapshot) {
579                 File snapshotDest = new File(avdFolder, SNAPSHOTS_IMG);
580                 if (snapshotDest.isFile() && editExisting) {
581                     log.printf("Snapshot image already present, was not changed.\n");
582 
583                 } else {
584                     String toolsLib = mSdkManager.getLocation() + File.separator
585                                       + SdkConstants.OS_SDK_TOOLS_LIB_EMULATOR_FOLDER;
586                     File snapshotBlank = new File(toolsLib, SNAPSHOTS_IMG);
587                     if (snapshotBlank.exists() == false) {
588                         log.error(null,
589                                 "Unable to find a '%2$s%1$s' file to copy into the AVD folder.",
590                                 SNAPSHOTS_IMG, toolsLib);
591                         needCleanup = true;
592                         return null;
593                     }
594 
595                     copyImageFile(snapshotBlank, snapshotDest);
596                 }
597                 values.put(AVD_INI_SNAPSHOT_PRESENT, "true");
598             }
599 
600             // Now the abi type
601             values.put(AVD_INI_ABI_TYPE, abiType);
602 
603             // and the cpu arch.
604             if (SdkConstants.ABI_ARMEABI.equals(abiType)) {
605                 values.put(AVD_INI_CPU_ARCH, SdkConstants.CPU_ARCH_ARM);
606             } else if (SdkConstants.ABI_ARMEABI_V7A.equals(abiType)) {
607                 values.put(AVD_INI_CPU_ARCH, SdkConstants.CPU_ARCH_ARM);
608                 values.put(AVD_INI_CPU_MODEL, SdkConstants.CPU_MODEL_CORTEX_A8);
609             } else if (SdkConstants.ABI_INTEL_ATOM.equals(abiType)) {
610                 values.put(AVD_INI_CPU_ARCH, SdkConstants.CPU_ARCH_INTEL_ATOM);
611             } else {
612                 log.error(null,
613                         "ABI %1$s is not supported by this version of the SDK Tools", abiType);
614                 needCleanup = true;
615                 return null;
616             }
617 
618             // Now the skin.
619             if (skinName == null || skinName.length() == 0) {
620                 skinName = target.getDefaultSkin();
621             }
622 
623             if (NUMERIC_SKIN_SIZE.matcher(skinName).matches()) {
624                 // Skin name is an actual screen resolution.
625                 // Set skin.name for display purposes in the AVD manager and
626                 // set skin.path for use by the emulator.
627                 values.put(AVD_INI_SKIN_NAME, skinName);
628                 values.put(AVD_INI_SKIN_PATH, skinName);
629             } else {
630                 // get the path of the skin (relative to the SDK)
631                 // assume skin name is valid
632                 String skinPath = getSkinRelativePath(skinName, target, log);
633                 if (skinPath == null) {
634                     log.error(null, "Missing skinpath in the AVD folder.");
635                     needCleanup = true;
636                     return null;
637                 }
638 
639                 values.put(AVD_INI_SKIN_PATH, skinPath);
640                 values.put(AVD_INI_SKIN_NAME, skinName);
641             }
642 
643             if (sdcard != null && sdcard.length() > 0) {
644                 // Sdcard is possibly a size. In that case we create a file called 'sdcard.img'
645                 // in the AVD folder, and do not put any value in config.ini.
646 
647                 long sdcardSize = parseSdcardSize(sdcard, null/*parsedStrings*/);
648 
649                 if (sdcardSize == SDCARD_SIZE_NOT_IN_RANGE) {
650                     log.error(null, "SD Card size must be in the range 9 MiB..1023 GiB.");
651                     needCleanup = true;
652                     return null;
653 
654                 } else if (sdcardSize == SDCARD_SIZE_INVALID) {
655                     log.error(null, "Unable to parse SD Card size");
656                     needCleanup = true;
657                     return null;
658 
659                 } else if (sdcardSize == SDCARD_NOT_SIZE_PATTERN) {
660                     File sdcardFile = new File(sdcard);
661                     if (sdcardFile.isFile()) {
662                         // sdcard value is an external sdcard, so we put its path into the config.ini
663                         values.put(AVD_INI_SDCARD_PATH, sdcard);
664                     } else {
665                         log.error(null, "'%1$s' is not recognized as a valid sdcard value.\n"
666                                 + "Value should be:\n" + "1. path to an sdcard.\n"
667                                 + "2. size of the sdcard to create: <size>[K|M]", sdcard);
668                         needCleanup = true;
669                         return null;
670                     }
671                 } else {
672                     // create the sdcard.
673                     File sdcardFile = new File(avdFolder, SDCARD_IMG);
674 
675                     boolean runMkSdcard = true;
676                     if (sdcardFile.exists()) {
677                         if (sdcardFile.length() == sdcardSize && editExisting) {
678                             // There's already an sdcard file with the right size and we're
679                             // not overriding it... so don't remove it.
680                             runMkSdcard = false;
681                             log.printf("SD Card already present with same size, was not changed.\n");
682                         }
683                     }
684 
685                     if (runMkSdcard) {
686                         String path = sdcardFile.getAbsolutePath();
687 
688                         // execute mksdcard with the proper parameters.
689                         File toolsFolder = new File(mSdkManager.getLocation(),
690                                 SdkConstants.FD_TOOLS);
691                         File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName());
692 
693                         if (mkSdCard.isFile() == false) {
694                             log.error(null, "'%1$s' is missing from the SDK tools folder.",
695                                     mkSdCard.getName());
696                             needCleanup = true;
697                             return null;
698                         }
699 
700                         if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) {
701                             log.error(null, "Failed to create sdcard in the AVD folder.");
702                             needCleanup = true;
703                             return null; // mksdcard output has already been displayed, no need to
704                                          // output anything else.
705                         }
706                     }
707 
708                     // add a property containing the size of the sdcard for display purpose
709                     // only when the dev does 'android list avd'
710                     values.put(AVD_INI_SDCARD_SIZE, sdcard);
711                 }
712             }
713 
714             // add the hardware config to the config file.
715             // priority order is:
716             // - values provided by the user
717             // - values provided by the skin
718             // - values provided by the target (add-on only).
719             // In order to follow this priority, we'll add the lowest priority values first and then
720             // override by higher priority values.
721             // In the case of a platform with override values from the user, the skin value might
722             // already be there, but it's ok.
723 
724             HashMap<String, String> finalHardwareValues = new HashMap<String, String>();
725 
726             FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(),
727                     AvdManager.HARDWARE_INI);
728             if (targetHardwareFile.isFile()) {
729                 Map<String, String> targetHardwareConfig = ProjectProperties.parsePropertyFile(
730                         targetHardwareFile, log);
731 
732                 if (targetHardwareConfig != null) {
733                     finalHardwareValues.putAll(targetHardwareConfig);
734                     values.putAll(targetHardwareConfig);
735                 }
736             }
737 
738             // get the hardware properties for this skin
739             File skinFolder = getSkinPath(skinName, target);
740             FileWrapper skinHardwareFile = new FileWrapper(skinFolder, AvdManager.HARDWARE_INI);
741             if (skinHardwareFile.isFile()) {
742                 Map<String, String> skinHardwareConfig = ProjectProperties.parsePropertyFile(
743                         skinHardwareFile, log);
744 
745                 if (skinHardwareConfig != null) {
746                     finalHardwareValues.putAll(skinHardwareConfig);
747                     values.putAll(skinHardwareConfig);
748                 }
749             }
750 
751             // finally put the hardware provided by the user.
752             if (hardwareConfig != null) {
753                 finalHardwareValues.putAll(hardwareConfig);
754                 values.putAll(hardwareConfig);
755             }
756 
757             File configIniFile = new File(avdFolder, CONFIG_INI);
758             writeIniFile(configIniFile, values);
759 
760             // Generate the log report first because we want to control where line breaks
761             // are located when generating the hardware config list.
762             StringBuilder report = new StringBuilder();
763 
764             if (target.isPlatform()) {
765                 if (editExisting) {
766                     report.append(String.format("Updated AVD '%1$s' based on %2$s",
767                             avdName, target.getName()));
768                 } else {
769                     report.append(String.format("Created AVD '%1$s' based on %2$s",
770                             avdName, target.getName()));
771                 }
772             } else {
773                 if (editExisting) {
774                     report.append(String.format("Updated AVD '%1$s' based on %2$s (%3$s)", avdName,
775                             target.getName(), target.getVendor()));
776                 } else {
777                     report.append(String.format("Created AVD '%1$s' based on %2$s (%3$s)", avdName,
778                             target.getName(), target.getVendor()));
779                 }
780             }
781             report.append(String.format(", %s processor", AvdInfo.getPrettyAbiType(abiType)));
782 
783             // display the chosen hardware config
784             if (finalHardwareValues.size() > 0) {
785                 report.append(",\nwith the following hardware config:\n");
786                 for (Entry<String, String> entry : finalHardwareValues.entrySet()) {
787                     report.append(String.format("%s=%s\n",entry.getKey(), entry.getValue()));
788                 }
789             } else {
790                 report.append("\n");
791             }
792 
793             log.printf(report.toString());
794 
795             // create the AvdInfo object, and add it to the list
796             AvdInfo newAvdInfo = new AvdInfo(
797                     avdName,
798                     iniFile,
799                     avdFolder.getAbsolutePath(),
800                     target.hashString(),
801                     target, abiType, values);
802 
803             AvdInfo oldAvdInfo = getAvd(avdName, false /*validAvdOnly*/);
804 
805             synchronized (mAllAvdList) {
806                 if (oldAvdInfo != null && (removePrevious || editExisting)) {
807                     mAllAvdList.remove(oldAvdInfo);
808                 }
809                 mAllAvdList.add(newAvdInfo);
810                 mValidAvdList = mBrokenAvdList = null;
811             }
812 
813             if ((removePrevious || editExisting) &&
814                     newAvdInfo != null &&
815                     oldAvdInfo != null &&
816                     !oldAvdInfo.getDataFolderPath().equals(newAvdInfo.getDataFolderPath())) {
817                 log.warning("Removing previous AVD directory at %s",
818                         oldAvdInfo.getDataFolderPath());
819                 // Remove the old data directory
820                 File dir = new File(oldAvdInfo.getDataFolderPath());
821                 try {
822                     deleteContentOf(dir);
823                     dir.delete();
824                 } catch (SecurityException e) {
825                     log.error(e, "Failed to delete %1$s", dir.getAbsolutePath());
826                 }
827             }
828 
829             return newAvdInfo;
830         } catch (AndroidLocationException e) {
831             log.error(e, null);
832         } catch (IOException e) {
833             log.error(e, null);
834         } catch (SecurityException e) {
835             log.error(e, null);
836         } finally {
837             if (needCleanup) {
838                 if (iniFile != null && iniFile.exists()) {
839                     iniFile.delete();
840                 }
841 
842                 try {
843                     deleteContentOf(avdFolder);
844                     avdFolder.delete();
845                 } catch (SecurityException e) {
846                     log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath());
847                 }
848             }
849         }
850 
851         return null;
852     }
853 
854     /**
855      * Copy the nominated file to the given destination.
856      *
857      * @throws FileNotFoundException
858      * @throws IOException
859      */
copyImageFile(File source, File destination)860     private void copyImageFile(File source, File destination)
861             throws FileNotFoundException, IOException {
862         FileInputStream fis = new FileInputStream(source);
863         FileOutputStream fos = new FileOutputStream(destination);
864 
865         byte[] buffer = new byte[4096];
866         int count;
867         while ((count = fis.read(buffer)) != -1) {
868             fos.write(buffer, 0, count);
869         }
870 
871         fos.close();
872         fis.close();
873     }
874 
875     /**
876      * Returns the path to the target images folder as a relative path to the SDK, if the folder
877      * is not empty. If the image folder is empty or does not exist, <code>null</code> is returned.
878      * @throws InvalidTargetPathException if the target image folder is not in the current SDK.
879      */
getImageRelativePath(IAndroidTarget target, String abiType)880     private String getImageRelativePath(IAndroidTarget target, String abiType)
881             throws InvalidTargetPathException {
882 
883         ISystemImage systemImage = target.getSystemImage(abiType);
884         if (systemImage == null) {
885             throw new IllegalArgumentException(String.format(
886                     "ABI Type %s is unknown for target %s",
887                     abiType,
888                     target.getDescription()));
889         }
890 
891         File folder = systemImage.getLocation();
892         String imageFullPath = folder.getAbsolutePath();
893 
894         // make this path relative to the SDK location
895         String sdkLocation = mSdkManager.getLocation();
896         if (!imageFullPath.startsWith(sdkLocation)) {
897             // this really really should not happen.
898             assert false;
899             throw new InvalidTargetPathException("Target location is not inside the SDK.");
900         }
901 
902         if (folder.isDirectory()) {
903             String[] list = folder.list(new FilenameFilter() {
904                 public boolean accept(File dir, String name) {
905                     return IMAGE_NAME_PATTERN.matcher(name).matches();
906                 }
907             });
908 
909             if (list.length > 0) {
910                 // Remove the SDK root path, e.g. /sdk/dir1/dir2 => /dir1/dir2
911                 imageFullPath = imageFullPath.substring(sdkLocation.length());
912                 // The path is relative, so it must not start with a file separator
913                 if (imageFullPath.charAt(0) == File.separatorChar) {
914                     imageFullPath = imageFullPath.substring(1);
915                 }
916                 // For compatibility with previous versions, we denote folders
917                 // by ending the path with file separator
918                 if (!imageFullPath.endsWith(File.separator)) {
919                     imageFullPath += File.separator;
920                 }
921 
922                 return imageFullPath;
923             }
924         }
925 
926         return null;
927     }
928 
929     /**
930      * Returns the path to the skin, as a relative path to the SDK.
931      * @param skinName The name of the skin to find. Case-sensitive.
932      * @param target The target where to find the skin.
933      * @param log the log object to receive action logs. Cannot be null.
934      */
getSkinRelativePath(String skinName, IAndroidTarget target, ISdkLog log)935     public String getSkinRelativePath(String skinName, IAndroidTarget target, ISdkLog log) {
936         if (log == null) {
937             throw new IllegalArgumentException("log cannot be null");
938         }
939 
940         // first look to see if the skin is in the target
941         File skin = getSkinPath(skinName, target);
942 
943         // skin really does not exist!
944         if (skin.exists() == false) {
945             log.error(null, "Skin '%1$s' does not exist.", skinName);
946             return null;
947         }
948 
949         // get the skin path
950         String path = skin.getAbsolutePath();
951 
952         // make this path relative to the SDK location
953         String sdkLocation = mSdkManager.getLocation();
954         if (path.startsWith(sdkLocation) == false) {
955             // this really really should not happen.
956             log.error(null, "Target location is not inside the SDK.");
957             assert false;
958             return null;
959         }
960 
961         path = path.substring(sdkLocation.length());
962         if (path.charAt(0) == File.separatorChar) {
963             path = path.substring(1);
964         }
965         return path;
966     }
967 
968     /**
969      * Returns the full absolute OS path to a skin specified by name for a given target.
970      * @param skinName The name of the skin to find. Case-sensitive.
971      * @param target The target where to find the skin.
972      * @return a {@link File} that may or may not actually exist.
973      */
getSkinPath(String skinName, IAndroidTarget target)974     public File getSkinPath(String skinName, IAndroidTarget target) {
975         String path = target.getPath(IAndroidTarget.SKINS);
976         File skin = new File(path, skinName);
977 
978         if (skin.exists() == false && target.isPlatform() == false) {
979             target = target.getParent();
980 
981             path = target.getPath(IAndroidTarget.SKINS);
982             skin = new File(path, skinName);
983         }
984 
985         return skin;
986     }
987 
988     /**
989      * Creates the ini file for an AVD.
990      *
991      * @param name of the AVD.
992      * @param avdFolder path for the data folder of the AVD.
993      * @param target of the AVD.
994      * @param removePrevious True if an existing ini file should be removed.
995      * @throws AndroidLocationException if there's a problem getting android root directory.
996      * @throws IOException if {@link File#getAbsolutePath()} fails.
997      */
createAvdIniFile(String name, File avdFolder, IAndroidTarget target, boolean removePrevious)998     private File createAvdIniFile(String name,
999             File avdFolder,
1000             IAndroidTarget target,
1001             boolean removePrevious)
1002             throws AndroidLocationException, IOException {
1003         File iniFile = AvdInfo.getDefaultIniFile(this, name);
1004 
1005         if (removePrevious) {
1006             if (iniFile.isFile()) {
1007                 iniFile.delete();
1008             } else if (iniFile.isDirectory()) {
1009                 deleteContentOf(iniFile);
1010                 iniFile.delete();
1011             }
1012         }
1013 
1014         HashMap<String, String> values = new HashMap<String, String>();
1015         values.put(AVD_INFO_PATH, avdFolder.getAbsolutePath());
1016         values.put(AVD_INFO_TARGET, target.hashString());
1017         writeIniFile(iniFile, values);
1018 
1019         return iniFile;
1020     }
1021 
1022     /**
1023      * Creates the ini file for an AVD.
1024      *
1025      * @param info of the AVD.
1026      * @throws AndroidLocationException if there's a problem getting android root directory.
1027      * @throws IOException if {@link File#getAbsolutePath()} fails.
1028      */
createAvdIniFile(AvdInfo info)1029     private File createAvdIniFile(AvdInfo info) throws AndroidLocationException, IOException {
1030         return createAvdIniFile(info.getName(),
1031                 new File(info.getDataFolderPath()),
1032                 info.getTarget(),
1033                 false /*removePrevious*/);
1034     }
1035 
1036     /**
1037      * Actually deletes the files of an existing AVD.
1038      * <p/>
1039      * This also remove it from the manager's list, The caller does not need to
1040      * call {@link #removeAvd(AvdInfo)} afterwards.
1041      * <p/>
1042      * This method is designed to somehow work with an unavailable AVD, that is an AVD that
1043      * could not be loaded due to some error. That means this method still tries to remove
1044      * the AVD ini file or its folder if it can be found. An error will be output if any of
1045      * these operations fail.
1046      *
1047      * @param avdInfo the information on the AVD to delete
1048      * @param log the log object to receive action logs. Cannot be null.
1049      * @return True if the AVD was deleted with no error.
1050      */
deleteAvd(AvdInfo avdInfo, ISdkLog log)1051     public boolean deleteAvd(AvdInfo avdInfo, ISdkLog log) {
1052         try {
1053             boolean error = false;
1054 
1055             File f = avdInfo.getIniFile();
1056             if (f != null && f.exists()) {
1057                 log.printf("Deleting file %1$s\n", f.getCanonicalPath());
1058                 if (!f.delete()) {
1059                     log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath());
1060                     error = true;
1061                 }
1062             }
1063 
1064             String path = avdInfo.getDataFolderPath();
1065             if (path != null) {
1066                 f = new File(path);
1067                 if (f.exists()) {
1068                     log.printf("Deleting folder %1$s\n", f.getCanonicalPath());
1069                     if (deleteContentOf(f) == false || f.delete() == false) {
1070                         log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath());
1071                         error = true;
1072                     }
1073                 }
1074             }
1075 
1076             removeAvd(avdInfo);
1077 
1078             if (error) {
1079                 log.printf("\nAVD '%1$s' deleted with errors. See errors above.\n",
1080                         avdInfo.getName());
1081             } else {
1082                 log.printf("\nAVD '%1$s' deleted.\n", avdInfo.getName());
1083                 return true;
1084             }
1085 
1086         } catch (IOException e) {
1087             log.error(e, null);
1088         } catch (SecurityException e) {
1089             log.error(e, null);
1090         }
1091         return false;
1092     }
1093 
1094     /**
1095      * Moves and/or rename an existing AVD and its files.
1096      * This also change it in the manager's list.
1097      * <p/>
1098      * The caller should make sure the name or path given are valid, do not exist and are
1099      * actually different than current values.
1100      *
1101      * @param avdInfo the information on the AVD to move.
1102      * @param newName the new name of the AVD if non null.
1103      * @param paramFolderPath the new data folder if non null.
1104      * @param log the log object to receive action logs. Cannot be null.
1105      * @return True if the move succeeded or there was nothing to do.
1106      *         If false, this method will have had already output error in the log.
1107      */
moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log)1108     public boolean moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log) {
1109 
1110         try {
1111             if (paramFolderPath != null) {
1112                 File f = new File(avdInfo.getDataFolderPath());
1113                 log.warning("Moving '%1$s' to '%2$s'.",
1114                         avdInfo.getDataFolderPath(),
1115                         paramFolderPath);
1116                 if (!f.renameTo(new File(paramFolderPath))) {
1117                     log.error(null, "Failed to move '%1$s' to '%2$s'.",
1118                             avdInfo.getDataFolderPath(), paramFolderPath);
1119                     return false;
1120                 }
1121 
1122                 // update AVD info
1123                 AvdInfo info = new AvdInfo(
1124                         avdInfo.getName(),
1125                         avdInfo.getIniFile(),
1126                         paramFolderPath,
1127                         avdInfo.getTargetHash(),
1128                         avdInfo.getTarget(),
1129                         avdInfo.getAbiType(),
1130                         avdInfo.getProperties());
1131                 replaceAvd(avdInfo, info);
1132 
1133                 // update the ini file
1134                 createAvdIniFile(info);
1135             }
1136 
1137             if (newName != null) {
1138                 File oldIniFile = avdInfo.getIniFile();
1139                 File newIniFile = AvdInfo.getDefaultIniFile(this, newName);
1140 
1141                 log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath());
1142                 if (!oldIniFile.renameTo(newIniFile)) {
1143                     log.error(null, "Failed to move '%1$s' to '%2$s'.",
1144                             oldIniFile.getPath(), newIniFile.getPath());
1145                     return false;
1146                 }
1147 
1148                 // update AVD info
1149                 AvdInfo info = new AvdInfo(
1150                         newName,
1151                         avdInfo.getIniFile(),
1152                         avdInfo.getDataFolderPath(),
1153                         avdInfo.getTargetHash(),
1154                         avdInfo.getTarget(),
1155                         avdInfo.getAbiType(),
1156                         avdInfo.getProperties());
1157                 replaceAvd(avdInfo, info);
1158             }
1159 
1160             log.printf("AVD '%1$s' moved.\n", avdInfo.getName());
1161 
1162         } catch (AndroidLocationException e) {
1163             log.error(e, null);
1164         } catch (IOException e) {
1165             log.error(e, null);
1166         }
1167 
1168         // nothing to do or succeeded
1169         return true;
1170     }
1171 
1172     /**
1173      * Helper method to recursively delete a folder's content (but not the folder itself).
1174      *
1175      * @throws SecurityException like {@link File#delete()} does if file/folder is not writable.
1176      */
deleteContentOf(File folder)1177     private boolean deleteContentOf(File folder) throws SecurityException {
1178         File[] files = folder.listFiles();
1179         if (files != null) {
1180             for (File f : files) {
1181                 if (f.isDirectory()) {
1182                     if (deleteContentOf(f) == false) {
1183                         return false;
1184                     }
1185                 }
1186                 if (f.delete() == false) {
1187                     return false;
1188                 }
1189 
1190             }
1191         }
1192 
1193         return true;
1194     }
1195 
1196     /**
1197      * Returns a list of files that are potential AVD ini files.
1198      * <p/>
1199      * This lists the $HOME/.android/avd/<name>.ini files.
1200      * Such files are properties file than then indicate where the AVD folder is located.
1201      * <p/>
1202      * Note: the method is to be considered private. It is made protected so that
1203      * unit tests can easily override the AVD root.
1204      *
1205      * @return A new {@link File} array or null. The array might be empty.
1206      * @throws AndroidLocationException if there's a problem getting android root directory.
1207      */
buildAvdFilesList()1208     private File[] buildAvdFilesList() throws AndroidLocationException {
1209         File folder = new File(getBaseAvdFolder());
1210 
1211         // ensure folder validity.
1212         if (folder.isFile()) {
1213             throw new AndroidLocationException(
1214                     String.format("%1$s is not a valid folder.", folder.getAbsolutePath()));
1215         } else if (folder.exists() == false) {
1216             // folder is not there, we create it and return
1217             folder.mkdirs();
1218             return null;
1219         }
1220 
1221         File[] avds = folder.listFiles(new FilenameFilter() {
1222             public boolean accept(File parent, String name) {
1223                 if (INI_NAME_PATTERN.matcher(name).matches()) {
1224                     // check it's a file and not a folder
1225                     boolean isFile = new File(parent, name).isFile();
1226                     return isFile;
1227                 }
1228 
1229                 return false;
1230             }
1231         });
1232 
1233         return avds;
1234     }
1235 
1236     /**
1237      * Computes the internal list of available AVDs
1238      * @param allList the list to contain all the AVDs
1239      * @param log the log object to receive action logs. Cannot be null.
1240      *
1241      * @throws AndroidLocationException if there's a problem getting android root directory.
1242      */
buildAvdList(ArrayList<AvdInfo> allList, ISdkLog log)1243     private void buildAvdList(ArrayList<AvdInfo> allList, ISdkLog log)
1244             throws AndroidLocationException {
1245         File[] avds = buildAvdFilesList();
1246         if (avds != null) {
1247             for (File avd : avds) {
1248                 AvdInfo info = parseAvdInfo(avd, log);
1249                 if (info != null) {
1250                     allList.add(info);
1251                 }
1252             }
1253         }
1254     }
1255 
1256     /**
1257      * Parses an AVD .ini file to create an {@link AvdInfo}.
1258      *
1259      * @param iniPath The path to the AVD .ini file
1260      * @param log the log object to receive action logs. Cannot be null.
1261      * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is
1262      *         valid or not.
1263      */
parseAvdInfo(File iniPath, ISdkLog log)1264     private AvdInfo parseAvdInfo(File iniPath, ISdkLog log) {
1265         Map<String, String> map = ProjectProperties.parsePropertyFile(
1266                 new FileWrapper(iniPath),
1267                 log);
1268 
1269         String avdPath = map.get(AVD_INFO_PATH);
1270         String targetHash = map.get(AVD_INFO_TARGET);
1271 
1272         IAndroidTarget target = null;
1273         FileWrapper configIniFile = null;
1274         Map<String, String> properties = null;
1275 
1276         if (targetHash != null) {
1277             target = mSdkManager.getTargetFromHashString(targetHash);
1278         }
1279 
1280         // load the AVD properties.
1281         if (avdPath != null) {
1282             configIniFile = new FileWrapper(avdPath, CONFIG_INI);
1283         }
1284 
1285         if (configIniFile != null) {
1286             if (!configIniFile.isFile()) {
1287                 log.warning("Missing file '%1$s'.",  configIniFile.getPath());
1288             } else {
1289                 properties = ProjectProperties.parsePropertyFile(configIniFile, log);
1290             }
1291         }
1292 
1293         // get name
1294         String name = iniPath.getName();
1295         Matcher matcher = INI_NAME_PATTERN.matcher(iniPath.getName());
1296         if (matcher.matches()) {
1297             name = matcher.group(1);
1298         }
1299 
1300         // get abi type
1301         String abiType = properties == null ? null : properties.get(AVD_INI_ABI_TYPE);
1302         // for the avds created previously without enhancement, i.e. They are created based
1303         // on previous API Levels. They are supposed to have ARM processor type
1304         if (abiType == null) {
1305             abiType = SdkConstants.ABI_ARMEABI;
1306         }
1307 
1308         // check the image.sysdir are valid
1309         boolean validImageSysdir = true;
1310         if (properties != null) {
1311             String imageSysDir = properties.get(AVD_INI_IMAGES_1);
1312             if (imageSysDir != null) {
1313                 File f = new File(mSdkManager.getLocation() + File.separator + imageSysDir);
1314                 if (f.isDirectory() == false) {
1315                     validImageSysdir = false;
1316                 } else {
1317                     imageSysDir = properties.get(AVD_INI_IMAGES_2);
1318                     if (imageSysDir != null) {
1319                         f = new File(mSdkManager.getLocation() + File.separator + imageSysDir);
1320                         if (f.isDirectory() == false) {
1321                             validImageSysdir = false;
1322                         }
1323                     }
1324                 }
1325             }
1326         }
1327 
1328         // TODO: What about missing sdcard, skins, etc?
1329 
1330         AvdStatus status;
1331 
1332         if (avdPath == null) {
1333             status = AvdStatus.ERROR_PATH;
1334         } else if (configIniFile == null) {
1335             status = AvdStatus.ERROR_CONFIG;
1336         } else if (targetHash == null) {
1337             status = AvdStatus.ERROR_TARGET_HASH;
1338         } else if (target == null) {
1339             status = AvdStatus.ERROR_TARGET;
1340         } else if (properties == null) {
1341             status = AvdStatus.ERROR_PROPERTIES;
1342         } else if (validImageSysdir == false) {
1343             status = AvdStatus.ERROR_IMAGE_DIR;
1344         } else {
1345             status = AvdStatus.OK;
1346         }
1347 
1348         AvdInfo info = new AvdInfo(
1349                 name,
1350                 iniPath,
1351                 avdPath,
1352                 targetHash,
1353                 target,
1354                 abiType,
1355                 properties,
1356                 status);
1357 
1358         return info;
1359     }
1360 
1361     /**
1362      * Writes a .ini file from a set of properties, using UTF-8 encoding.
1363      *
1364      * @param iniFile The file to generate.
1365      * @param values THe properties to place in the ini file.
1366      * @throws IOException if {@link FileWriter} fails to open, write or close the file.
1367      */
writeIniFile(File iniFile, Map<String, String> values)1368     private static void writeIniFile(File iniFile, Map<String, String> values)
1369             throws IOException {
1370         OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile),
1371                 SdkConstants.INI_CHARSET);
1372 
1373         for (Entry<String, String> entry : values.entrySet()) {
1374             writer.write(String.format("%1$s=%2$s\n", entry.getKey(), entry.getValue()));
1375         }
1376         writer.close();
1377     }
1378 
1379     /**
1380      * Invokes the tool to create a new SD card image file.
1381      *
1382      * @param toolLocation The path to the mksdcard tool.
1383      * @param size The size of the new SD Card, compatible with {@link #SDCARD_SIZE_PATTERN}.
1384      * @param location The path of the new sdcard image file to generate.
1385      * @param log the log object to receive action logs. Cannot be null.
1386      * @return True if the sdcard could be created.
1387      */
createSdCard(String toolLocation, String size, String location, ISdkLog log)1388     private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) {
1389         try {
1390             String[] command = new String[3];
1391             command[0] = toolLocation;
1392             command[1] = size;
1393             command[2] = location;
1394             Process process = Runtime.getRuntime().exec(command);
1395 
1396             ArrayList<String> errorOutput = new ArrayList<String>();
1397             ArrayList<String> stdOutput = new ArrayList<String>();
1398             int status = grabProcessOutput(process, errorOutput, stdOutput,
1399                     true /* waitForReaders */);
1400 
1401             if (status == 0) {
1402                 return true;
1403             } else {
1404                 for (String error : errorOutput) {
1405                     log.error(null, error);
1406                 }
1407             }
1408 
1409         } catch (InterruptedException e) {
1410             // pass, print error below
1411         } catch (IOException e) {
1412             // pass, print error below
1413         }
1414 
1415         log.error(null, "Failed to create the SD card.");
1416         return false;
1417     }
1418 
1419     /**
1420      * Gets the stderr/stdout outputs of a process and returns when the process is done.
1421      * Both <b>must</b> be read or the process will block on windows.
1422      * @param process The process to get the ouput from
1423      * @param errorOutput The array to store the stderr output. cannot be null.
1424      * @param stdOutput The array to store the stdout output. cannot be null.
1425      * @param waitforReaders if true, this will wait for the reader threads.
1426      * @return the process return code.
1427      * @throws InterruptedException
1428      */
grabProcessOutput(final Process process, final ArrayList<String> errorOutput, final ArrayList<String> stdOutput, boolean waitforReaders)1429     private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput,
1430             final ArrayList<String> stdOutput, boolean waitforReaders)
1431             throws InterruptedException {
1432         assert errorOutput != null;
1433         assert stdOutput != null;
1434         // read the lines as they come. if null is returned, it's
1435         // because the process finished
1436         Thread t1 = new Thread("") { //$NON-NLS-1$
1437             @Override
1438             public void run() {
1439                 // create a buffer to read the stderr output
1440                 InputStreamReader is = new InputStreamReader(process.getErrorStream());
1441                 BufferedReader errReader = new BufferedReader(is);
1442 
1443                 try {
1444                     while (true) {
1445                         String line = errReader.readLine();
1446                         if (line != null) {
1447                             errorOutput.add(line);
1448                         } else {
1449                             break;
1450                         }
1451                     }
1452                 } catch (IOException e) {
1453                     // do nothing.
1454                 }
1455             }
1456         };
1457 
1458         Thread t2 = new Thread("") { //$NON-NLS-1$
1459             @Override
1460             public void run() {
1461                 InputStreamReader is = new InputStreamReader(process.getInputStream());
1462                 BufferedReader outReader = new BufferedReader(is);
1463 
1464                 try {
1465                     while (true) {
1466                         String line = outReader.readLine();
1467                         if (line != null) {
1468                             stdOutput.add(line);
1469                         } else {
1470                             break;
1471                         }
1472                     }
1473                 } catch (IOException e) {
1474                     // do nothing.
1475                 }
1476             }
1477         };
1478 
1479         t1.start();
1480         t2.start();
1481 
1482         // it looks like on windows process#waitFor() can return
1483         // before the thread have filled the arrays, so we wait for both threads and the
1484         // process itself.
1485         if (waitforReaders) {
1486             try {
1487                 t1.join();
1488             } catch (InterruptedException e) {
1489                 // nothing to do here
1490             }
1491             try {
1492                 t2.join();
1493             } catch (InterruptedException e) {
1494                 // nothing to do here
1495             }
1496         }
1497 
1498         // get the return code from the process
1499         return process.waitFor();
1500     }
1501 
1502     /**
1503      * Removes an {@link AvdInfo} from the internal list.
1504      *
1505      * @param avdInfo The {@link AvdInfo} to remove.
1506      * @return true if this {@link AvdInfo} was present and has been removed.
1507      */
removeAvd(AvdInfo avdInfo)1508     public boolean removeAvd(AvdInfo avdInfo) {
1509         synchronized (mAllAvdList) {
1510             if (mAllAvdList.remove(avdInfo)) {
1511                 mValidAvdList = mBrokenAvdList = null;
1512                 return true;
1513             }
1514         }
1515 
1516         return false;
1517     }
1518 
1519     /**
1520      * Updates an AVD with new path to the system image folders.
1521      * @param name the name of the AVD to update.
1522      * @param log the log object to receive action logs. Cannot be null.
1523      * @throws IOException
1524      */
updateAvd(String name, ISdkLog log)1525     public void updateAvd(String name, ISdkLog log) throws IOException {
1526         // find the AVD to update. It should be be in the broken list.
1527         AvdInfo avd = null;
1528         synchronized (mAllAvdList) {
1529             for (AvdInfo info : mAllAvdList) {
1530                 if (info.getName().equals(name)) {
1531                     avd = info;
1532                     break;
1533                 }
1534             }
1535         }
1536 
1537         if (avd == null) {
1538             // not in the broken list, just return.
1539             log.error(null, "There is no Android Virtual Device named '%s'.", name);
1540             return;
1541         }
1542 
1543         updateAvd(avd, log);
1544     }
1545 
1546 
1547     /**
1548      * Updates an AVD with new path to the system image folders.
1549      * @param avd the AVD to update.
1550      * @param log the log object to receive action logs. Cannot be null.
1551      * @throws IOException
1552      */
updateAvd(AvdInfo avd, ISdkLog log)1553     public void updateAvd(AvdInfo avd, ISdkLog log) throws IOException {
1554         // get the properties. This is a unmodifiable Map.
1555         Map<String, String> oldProperties = avd.getProperties();
1556 
1557         // create a new map
1558         Map<String, String> properties = new HashMap<String, String>();
1559         if (oldProperties != null) {
1560             properties.putAll(oldProperties);
1561         }
1562 
1563         AvdStatus status;
1564 
1565         // create the path to the new system images.
1566         if (setImagePathProperties(avd.getTarget(), avd.getAbiType(), properties, log)) {
1567             if (properties.containsKey(AVD_INI_IMAGES_1)) {
1568                 log.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_1,
1569                         properties.get(AVD_INI_IMAGES_1));
1570             }
1571 
1572             if (properties.containsKey(AVD_INI_IMAGES_2)) {
1573                 log.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_2,
1574                         properties.get(AVD_INI_IMAGES_2));
1575             }
1576 
1577             status = AvdStatus.OK;
1578         } else {
1579             log.error(null, "Unable to find non empty system images folders for %1$s",
1580                     avd.getName());
1581             //FIXME: display paths to empty image folders?
1582             status = AvdStatus.ERROR_IMAGE_DIR;
1583         }
1584 
1585         // now write the config file
1586         File configIniFile = new File(avd.getDataFolderPath(), CONFIG_INI);
1587         writeIniFile(configIniFile, properties);
1588 
1589         // finally create a new AvdInfo for this unbroken avd and add it to the list.
1590         // instead of creating the AvdInfo object directly we reparse it, to detect other possible
1591         // errors
1592         // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect other errors.
1593         AvdInfo newAvd = new AvdInfo(
1594                 avd.getName(),
1595                 avd.getIniFile(),
1596                 avd.getDataFolderPath(),
1597                 avd.getTargetHash(),
1598                 avd.getTarget(),
1599                 avd.getAbiType(),
1600                 properties,
1601                 status);
1602 
1603         replaceAvd(avd, newAvd);
1604     }
1605 
1606     /**
1607      * Sets the paths to the system images in a properties map.
1608      *
1609      * @param target the target in which to find the system images.
1610      * @param abiType the abi type of the avd to find
1611      *        the architecture-dependent system images.
1612      * @param properties the properties in which to set the paths.
1613      * @param log the log object to receive action logs. Cannot be null.
1614      * @return true if success, false if some path are missing.
1615      */
setImagePathProperties(IAndroidTarget target, String abiType, Map<String, String> properties, ISdkLog log)1616     private boolean setImagePathProperties(IAndroidTarget target,
1617             String abiType,
1618             Map<String, String> properties,
1619             ISdkLog log) {
1620         properties.remove(AVD_INI_IMAGES_1);
1621         properties.remove(AVD_INI_IMAGES_2);
1622 
1623         try {
1624             String property = AVD_INI_IMAGES_1;
1625 
1626             // First the image folders of the target itself
1627             String imagePath = getImageRelativePath(target, abiType);
1628             if (imagePath != null) {
1629                 properties.put(property, imagePath);
1630                 property = AVD_INI_IMAGES_2;
1631             }
1632 
1633 
1634             // If the target is an add-on we need to add the Platform image as a backup.
1635             IAndroidTarget parent = target.getParent();
1636             if (parent != null) {
1637                 imagePath = getImageRelativePath(parent, abiType);
1638                 if (imagePath != null) {
1639                     properties.put(property, imagePath);
1640                 }
1641             }
1642 
1643             // we need at least one path!
1644             return properties.containsKey(AVD_INI_IMAGES_1);
1645         } catch (InvalidTargetPathException e) {
1646             log.error(e, e.getMessage());
1647         }
1648 
1649         return false;
1650     }
1651 
1652     /**
1653      * Replaces an old {@link AvdInfo} with a new one in the lists storing them.
1654      * @param oldAvd the {@link AvdInfo} to remove.
1655      * @param newAvd the {@link AvdInfo} to add.
1656      */
replaceAvd(AvdInfo oldAvd, AvdInfo newAvd)1657     private void replaceAvd(AvdInfo oldAvd, AvdInfo newAvd) {
1658         synchronized (mAllAvdList) {
1659             mAllAvdList.remove(oldAvd);
1660             mAllAvdList.add(newAvd);
1661             mValidAvdList = mBrokenAvdList = null;
1662         }
1663     }
1664 }
1665