--- /dev/null
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.internal.avd;
+
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.ISdkLog;
+import com.android.sdklib.SdkConstants;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.internal.avd.AvdManager.AvdInfo.AvdStatus;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdklib.io.FileWrapper;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Android Virtual Device Manager to manage AVDs.
+ */
+public final class AvdManager {
+
+ /**
+ * Exception thrown when something is wrong with a target path.
+ */
+ private final static class InvalidTargetPathException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ InvalidTargetPathException(String message) {
+ super(message);
+ }
+ }
+
+ public static final String AVD_FOLDER_EXTENSION = ".avd"; //$NON-NLS-1$
+
+ public final static String AVD_INFO_PATH = "path"; //$NON-NLS-1$
+ public final static String AVD_INFO_TARGET = "target"; //$NON-NLS-1$
+
+ /**
+ * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any,
+ * or a 320x480 like constant for a numeric skin size.
+ *
+ * @see #NUMERIC_SKIN_SIZE
+ */
+ public final static String AVD_INI_SKIN_PATH = "skin.path"; //$NON-NLS-1$
+ /**
+ * AVD/config.ini key name representing an UI name for the skin.
+ * This config key is ignored by the emulator. It is only used by the SDK manager or
+ * tools to give a friendlier name to the skin.
+ * If missing, use the {@link #AVD_INI_SKIN_PATH} key instead.
+ */
+ public final static String AVD_INI_SKIN_NAME = "skin.name"; //$NON-NLS-1$
+ /**
+ * AVD/config.ini key name representing the path to the sdcard file.
+ * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such
+ * a file.
+ *
+ * @see #SDCARD_IMG
+ */
+ public final static String AVD_INI_SDCARD_PATH = "sdcard.path"; //$NON-NLS-1$
+ /**
+ * AVD/config.ini key name representing the size of the SD card.
+ * This property is for UI purposes only. It is not used by the emulator.
+ *
+ * @see #SDCARD_SIZE_PATTERN
+ */
+ public final static String AVD_INI_SDCARD_SIZE = "sdcard.size"; //$NON-NLS-1$
+ /**
+ * AVD/config.ini key name representing the first path where the emulator looks
+ * for system images. Typically this is the path to the add-on system image or
+ * the path to the platform system image if there's no add-on.
+ * <p/>
+ * The emulator looks at {@link #AVD_INI_IMAGES_1} before {@link #AVD_INI_IMAGES_2}.
+ */
+ public final static String AVD_INI_IMAGES_1 = "image.sysdir.1"; //$NON-NLS-1$
+ /**
+ * AVD/config.ini key name representing the second path where the emulator looks
+ * for system images. Typically this is the path to the platform system image.
+ *
+ * @see #AVD_INI_IMAGES_1
+ */
+ public final static String AVD_INI_IMAGES_2 = "image.sysdir.2"; //$NON-NLS-1$
+ /**
+ * AVD/config.ini key name representing the presence of the snapshots file.
+ * This property is for UI purposes only. It is not used by the emulator.
+ *
+ * @see #SNAPSHOTS_IMG
+ */
+ public final static String AVD_INI_SNAPSHOT_PRESENT = "snapshot.present"; //$NON-NLS-1$
+
+ /**
+ * Pattern to match pixel-sized skin "names", e.g. "320x480".
+ */
+ public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$
+
+ private final static String USERDATA_IMG = "userdata.img"; //$NON-NLS-1$
+ private final static String CONFIG_INI = "config.ini"; //$NON-NLS-1$
+ private final static String SDCARD_IMG = "sdcard.img"; //$NON-NLS-1$
+ private final static String SNAPSHOTS_IMG = "snapshots.img"; //$NON-NLS-1$
+
+ private final static String INI_EXTENSION = ".ini"; //$NON-NLS-1$
+ private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + //$NON-NLS-1$
+ INI_EXTENSION + "$", //$NON-NLS-1$
+ Pattern.CASE_INSENSITIVE);
+
+ private final static Pattern IMAGE_NAME_PATTERN = Pattern.compile("(.+)\\.img$", //$NON-NLS-1$
+ Pattern.CASE_INSENSITIVE);
+
+ /**
+ * Pattern for matching SD Card sizes, e.g. "4K" or "16M".
+ */
+ public final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("(\\d+)([MK])"); //$NON-NLS-1$
+
+ /** Regex used to validate characters that compose an AVD name. */
+ public final static Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); //$NON-NLS-1$
+
+ /** List of valid characters for an AVD name. Used for display purposes. */
+ public final static String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; //$NON-NLS-1$
+
+ public final static String HARDWARE_INI = "hardware.ini"; //$NON-NLS-1$
+
+ /** An immutable structure describing an Android Virtual Device. */
+ public static final class AvdInfo implements Comparable<AvdInfo> {
+
+ /**
+ * Status for an {@link AvdInfo}. Indicates whether or not this AVD is valid.
+ */
+ public static enum AvdStatus {
+ /** No error */
+ OK,
+ /** Missing 'path' property in the ini file */
+ ERROR_PATH,
+ /** Missing config.ini file in the AVD data folder */
+ ERROR_CONFIG,
+ /** Missing 'target' property in the ini file */
+ ERROR_TARGET_HASH,
+ /** Target was not resolved from its hash */
+ ERROR_TARGET,
+ /** Unable to parse config.ini */
+ ERROR_PROPERTIES,
+ /** System Image folder in config.ini doesn't exist */
+ ERROR_IMAGE_DIR;
+ }
+
+ private final String mName;
+ private final String mPath;
+ private final String mTargetHash;
+ private final IAndroidTarget mTarget;
+ private final Map<String, String> mProperties;
+ private final AvdStatus mStatus;
+
+ /**
+ * Creates a new valid AVD info. Values are immutable.
+ * <p/>
+ * Such an AVD is available and can be used.
+ * The error string is set to null.
+ *
+ * @param name The name of the AVD (for display or reference)
+ * @param path The path to the config.ini file
+ * @param targetHash the target hash
+ * @param target The target. Can be null, if the target was not resolved.
+ * @param properties The property map. Cannot be null.
+ */
+ public AvdInfo(String name, String path, String targetHash, IAndroidTarget target,
+ Map<String, String> properties) {
+ this(name, path, targetHash, target, properties, AvdStatus.OK);
+ }
+
+ /**
+ * Creates a new <em>invalid</em> AVD info. Values are immutable.
+ * <p/>
+ * Such an AVD is not complete and cannot be used.
+ * The error string must be non-null.
+ *
+ * @param name The name of the AVD (for display or reference)
+ * @param path The path to the config.ini file
+ * @param targetHash the target hash
+ * @param target The target. Can be null, if the target was not resolved.
+ * @param properties The property map. Can be null.
+ * @param status The {@link AvdStatus} of this AVD. Cannot be null.
+ */
+ public AvdInfo(String name, String path, String targetHash, IAndroidTarget target,
+ Map<String, String> properties, AvdStatus status) {
+ mName = name;
+ mPath = path;
+ mTargetHash = targetHash;
+ mTarget = target;
+ mProperties = properties == null ? null : Collections.unmodifiableMap(properties);
+ mStatus = status;
+ }
+
+ /** Returns the name of the AVD. */
+ public String getName() {
+ return mName;
+ }
+
+ /** Returns the path of the AVD data directory. */
+ public String getPath() {
+ return mPath;
+ }
+
+ /**
+ * Returns the target hash string.
+ */
+ public String getTargetHash() {
+ return mTargetHash;
+ }
+
+ /** Returns the target of the AVD, or <code>null</code> if it has not been resolved. */
+ public IAndroidTarget getTarget() {
+ return mTarget;
+ }
+
+ /** Returns the {@link AvdStatus} of the receiver. */
+ public AvdStatus getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Helper method that returns the .ini {@link File} for a given AVD name.
+ * @throws AndroidLocationException if there's a problem getting android root directory.
+ */
+ public static File getIniFile(String name) throws AndroidLocationException {
+ String avdRoot;
+ avdRoot = getBaseAvdFolder();
+ return new File(avdRoot, name + INI_EXTENSION);
+ }
+
+ /**
+ * Returns the .ini {@link File} for this AVD.
+ * @throws AndroidLocationException if there's a problem getting android root directory.
+ */
+ public File getIniFile() throws AndroidLocationException {
+ return getIniFile(mName);
+ }
+
+ /**
+ * Helper method that returns the Config {@link File} for a given AVD name.
+ */
+ public static File getConfigFile(String path) {
+ return new File(path, CONFIG_INI);
+ }
+
+ /**
+ * Returns the Config {@link File} for this AVD.
+ */
+ public File getConfigFile() {
+ return getConfigFile(mPath);
+ }
+
+ /**
+ * Returns an unmodifiable map of properties for the AVD. This can be null.
+ */
+ public Map<String, String> getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * Returns the error message for the AVD or <code>null</code> if {@link #getStatus()}
+ * returns {@link AvdStatus#OK}
+ */
+ public String getErrorMessage() {
+ try {
+ switch (mStatus) {
+ case ERROR_PATH:
+ return String.format("Missing AVD 'path' property in %1$s", getIniFile());
+ case ERROR_CONFIG:
+ return String.format("Missing config.ini file in %1$s", mPath);
+ case ERROR_TARGET_HASH:
+ return String.format("Missing 'target' property in %1$s", getIniFile());
+ case ERROR_TARGET:
+ return String.format("Unknown target '%1$s' in %2$s",
+ mTargetHash, getIniFile());
+ case ERROR_PROPERTIES:
+ return String.format("Failed to parse properties from %1$s",
+ getConfigFile());
+ case ERROR_IMAGE_DIR:
+ return String.format(
+ "Invalid value in image.sysdir. Run 'android update avd -n %1$s'",
+ mName);
+ case OK:
+ assert false;
+ return null;
+ }
+ } catch (AndroidLocationException e) {
+ return "Unable to get HOME folder.";
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether an emulator is currently running the AVD.
+ */
+ public boolean isRunning() {
+ File f = new File(mPath, "userdata-qemu.img.lock");
+ return f.isFile();
+ }
+
+ /**
+ * Compares this object with the specified object for order. Returns a
+ * negative integer, zero, or a positive integer as this object is less
+ * than, equal to, or greater than the specified object.
+ *
+ * @param o the Object to be compared.
+ * @return a negative integer, zero, or a positive integer as this object is
+ * less than, equal to, or greater than the specified object.
+ */
+ public int compareTo(AvdInfo o) {
+ // first handle possible missing targets (if the AVD failed to load for
+ // unresolved targets.
+ if (mTarget == null) {
+ return +1;
+ } else if (o.mTarget == null) {
+ return -1;
+ }
+
+ // then compare the targets
+ int targetDiff = mTarget.compareTo(o.mTarget);
+
+ if (targetDiff == 0) {
+ // same target? compare on the avd name
+ return mName.compareTo(o.mName);
+ }
+
+ return targetDiff;
+ }
+ }
+
+ private final ArrayList<AvdInfo> mAllAvdList = new ArrayList<AvdInfo>();
+ private AvdInfo[] mValidAvdList;
+ private AvdInfo[] mBrokenAvdList;
+ private final SdkManager mSdkManager;
+
+ /**
+ * Returns the base folder where AVDs are created.
+ *
+ * @throws AndroidLocationException
+ */
+ public static String getBaseAvdFolder() throws AndroidLocationException {
+ return AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD;
+ }
+
+ /**
+ * Creates an AVD Manager for a given SDK represented by a {@link SdkManager}.
+ * @param sdkManager The SDK.
+ * @param log The log object to receive the log of the initial loading of the AVDs.
+ * This log object is not kept by this instance of AvdManager and each
+ * method takes its own logger. The rationale is that the AvdManager
+ * might be called from a variety of context, each with different
+ * logging needs. Cannot be null.
+ * @throws AndroidLocationException
+ */
+ public AvdManager(SdkManager sdkManager, ISdkLog log) throws AndroidLocationException {
+ mSdkManager = sdkManager;
+ buildAvdList(mAllAvdList, log);
+ }
+
+ /**
+ * Returns the {@link SdkManager} associated with the {@link AvdManager}.
+ */
+ public SdkManager getSdkManager() {
+ return mSdkManager;
+ }
+
+ /**
+ * Returns all the existing AVDs.
+ * @return a newly allocated array containing all the AVDs.
+ */
+ public AvdInfo[] getAllAvds() {
+ synchronized (mAllAvdList) {
+ return mAllAvdList.toArray(new AvdInfo[mAllAvdList.size()]);
+ }
+ }
+
+ /**
+ * Returns all the valid AVDs.
+ * @return a newly allocated array containing all valid the AVDs.
+ */
+ public AvdInfo[] getValidAvds() {
+ synchronized (mAllAvdList) {
+ if (mValidAvdList == null) {
+ ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
+ for (AvdInfo avd : mAllAvdList) {
+ if (avd.getStatus() == AvdStatus.OK) {
+ list.add(avd);
+ }
+ }
+
+ mValidAvdList = list.toArray(new AvdInfo[list.size()]);
+ }
+ return mValidAvdList;
+ }
+ }
+
+ /**
+ * Returns all the broken AVDs.
+ * @return a newly allocated array containing all the broken AVDs.
+ */
+ public AvdInfo[] getBrokenAvds() {
+ synchronized (mAllAvdList) {
+ if (mBrokenAvdList == null) {
+ ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
+ for (AvdInfo avd : mAllAvdList) {
+ if (avd.getStatus() != AvdStatus.OK) {
+ list.add(avd);
+ }
+ }
+ mBrokenAvdList = list.toArray(new AvdInfo[list.size()]);
+ }
+ return mBrokenAvdList;
+ }
+ }
+
+ /**
+ * Returns the {@link AvdInfo} matching the given <var>name</var>.
+ * <p/>
+ * The search is case-insensitive.
+ *
+ * @param name the name of the AVD to return
+ * @param validAvdOnly if <code>true</code>, only look through the list of valid AVDs.
+ * @return the matching AvdInfo or <code>null</code> if none were found.
+ */
+ public AvdInfo getAvd(String name, boolean validAvdOnly) {
+
+ boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS;
+
+ if (validAvdOnly) {
+ for (AvdInfo info : getValidAvds()) {
+ String name2 = info.getName();
+ if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
+ return info;
+ }
+ }
+ } else {
+ synchronized (mAllAvdList) {
+ for (AvdInfo info : mAllAvdList) {
+ String name2 = info.getName();
+ if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
+ return info;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reloads the AVD list.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @throws AndroidLocationException if there was an error finding the location of the
+ * AVD folder.
+ */
+ public void reloadAvds(ISdkLog log) throws AndroidLocationException {
+ // build the list in a temp list first, in case the method throws an exception.
+ // It's better than deleting the whole list before reading the new one.
+ ArrayList<AvdInfo> allList = new ArrayList<AvdInfo>();
+ buildAvdList(allList, log);
+
+ synchronized (mAllAvdList) {
+ mAllAvdList.clear();
+ mAllAvdList.addAll(allList);
+ mValidAvdList = mBrokenAvdList = null;
+ }
+ }
+
+ /**
+ * Creates a new AVD, but with no snapshot.
+ *
+ * See {@link #createAvd(File, String, IAndroidTarget, String, String, Map, boolean, boolean, ISdkLog)}
+ **/
+ @Deprecated
+ public AvdInfo createAvd(File avdFolder, String name, IAndroidTarget target, String skinName,
+ String sdcard, Map<String, String> hardwareConfig, boolean removePrevious,
+ ISdkLog log) {
+ return createAvd(avdFolder, name, target, skinName, sdcard, hardwareConfig, removePrevious,
+ false, log);
+ }
+
+ /**
+ * Creates a new AVD. It is expected that there is no existing AVD with this name already.
+ *
+ * @param avdFolder the data folder for the AVD. It will be created as needed.
+ * @param name the name of the AVD
+ * @param target the target of the AVD
+ * @param skinName the name of the skin. Can be null. Must have been verified by caller.
+ * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to
+ * an existing sdcard image or a sdcard size (\d+, \d+K, \dM).
+ * @param hardwareConfig the hardware setup for the AVD. Can be null to use defaults.
+ * @param removePrevious If true remove any previous files.
+ * @param createSnapshot If true copy a blank snapshot image into the AVD.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @return The new {@link AvdInfo} in case of success (which has just been added to the
+ * internal list) or null in case of failure.
+ */
+ public AvdInfo createAvd(File avdFolder, String name, IAndroidTarget target,
+ String skinName, String sdcard, Map<String,String> hardwareConfig,
+ boolean removePrevious, boolean createSnapshot, ISdkLog log) {
+ if (log == null) {
+ throw new IllegalArgumentException("log cannot be null");
+ }
+
+ File iniFile = null;
+ boolean needCleanup = false;
+ try {
+ if (avdFolder.exists()) {
+ if (removePrevious) {
+ // AVD already exists and removePrevious is set, try to remove the
+ // directory's content first (but not the directory itself).
+ try {
+ deleteContentOf(avdFolder);
+ } catch (SecurityException e) {
+ log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath());
+ }
+ } else {
+ // AVD shouldn't already exist if removePrevious is false.
+ log.error(null,
+ "Folder %1$s is in the way. Use --force if you want to overwrite.",
+ avdFolder.getAbsolutePath());
+ return null;
+ }
+ } else {
+ // create the AVD folder.
+ avdFolder.mkdir();
+ }
+
+ // actually write the ini file
+ iniFile = createAvdIniFile(name, avdFolder, target);
+
+ // writes the userdata.img in it.
+ String imagePath = target.getPath(IAndroidTarget.IMAGES);
+ File userdataSrc = new File(imagePath, USERDATA_IMG);
+
+ if (userdataSrc.exists() == false && target.isPlatform() == false) {
+ imagePath = target.getParent().getPath(IAndroidTarget.IMAGES);
+ userdataSrc = new File(imagePath, USERDATA_IMG);
+ }
+
+ if (userdataSrc.exists() == false) {
+ log.error(null, "Unable to find a '%1$s' file to copy into the AVD folder.",
+ USERDATA_IMG);
+ needCleanup = true;
+ return null;
+ }
+ File userdataDest = new File(avdFolder, USERDATA_IMG);
+
+ copyImageFile(userdataSrc, userdataDest);
+
+ // Config file.
+ HashMap<String, String> values = new HashMap<String, String>();
+
+ if (setImagePathProperties(target, values, log) == false) {
+ needCleanup = true;
+ return null;
+ }
+
+ // Create the snapshot file
+ if (createSnapshot) {
+ String toolsLib = mSdkManager.getLocation() + File.separator
+ + SdkConstants.OS_SDK_TOOLS_LIB_EMULATOR_FOLDER;
+ File snapshotBlank = new File(toolsLib, SNAPSHOTS_IMG);
+ if (snapshotBlank.exists() == false) {
+ log.error(null, "Unable to find a '%2$s%1$s' file to copy into the AVD folder.",
+ SNAPSHOTS_IMG, toolsLib);
+ needCleanup = true;
+ return null;
+ }
+ File snapshotDest = new File(avdFolder, SNAPSHOTS_IMG);
+ copyImageFile(snapshotBlank, snapshotDest);
+ values.put(AVD_INI_SNAPSHOT_PRESENT, "true");
+ }
+
+ // Now the skin.
+ if (skinName == null || skinName.length() == 0) {
+ skinName = target.getDefaultSkin();
+ }
+
+ if (NUMERIC_SKIN_SIZE.matcher(skinName).matches()) {
+ // Skin name is an actual screen resolution.
+ // Set skin.name for display purposes in the AVD manager and
+ // set skin.path for use by the emulator.
+ values.put(AVD_INI_SKIN_NAME, skinName);
+ values.put(AVD_INI_SKIN_PATH, skinName);
+ } else {
+ // get the path of the skin (relative to the SDK)
+ // assume skin name is valid
+ String skinPath = getSkinRelativePath(skinName, target, log);
+ if (skinPath == null) {
+ needCleanup = true;
+ return null;
+ }
+
+ values.put(AVD_INI_SKIN_PATH, skinPath);
+ values.put(AVD_INI_SKIN_NAME, skinName);
+ }
+
+ if (sdcard != null && sdcard.length() > 0) {
+ File sdcardFile = new File(sdcard);
+ if (sdcardFile.isFile()) {
+ // sdcard value is an external sdcard, so we put its path into the config.ini
+ values.put(AVD_INI_SDCARD_PATH, sdcard);
+ } else {
+ // Sdcard is possibly a size. In that case we create a file called 'sdcard.img'
+ // in the AVD folder, and do not put any value in config.ini.
+
+ // First, check that it matches the pattern for sdcard size
+ Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard);
+ if (m.matches()) {
+ // get the sdcard values for checks
+ try {
+ long sdcardSize = Long.parseLong(m.group(1));
+
+ // prevent overflow: no more than 999GB
+ // 10 digit for MiB, 13 for KiB
+ int digitCount = m.group(1).length();
+
+ String sdcardSizeModifier = m.group(2);
+ if ("K".equals(sdcardSizeModifier)) {
+ sdcardSize *= 1024L;
+ } else { // must be "M" per the pattern
+ sdcardSize *= 1024L * 1024L;
+ digitCount += 3; // convert the number of digit into "KiB"
+ }
+
+ if (digitCount >= 13) {
+ log.error(null, "SD Card size is too big!");
+ needCleanup = true;
+ return null;
+ }
+
+ if (sdcardSize < 9 * 1024 * 1024) {
+ log.error(null, "SD Card size must be at least 9MB");
+ needCleanup = true;
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ // this should never happen since the string is validated
+ // by the regexp
+ log.error(null, "Unable to parse SD Card size");
+ needCleanup = true;
+ return null;
+ }
+
+ // create the sdcard.
+ sdcardFile = new File(avdFolder, SDCARD_IMG);
+ String path = sdcardFile.getAbsolutePath();
+
+ // execute mksdcard with the proper parameters.
+ File toolsFolder = new File(mSdkManager.getLocation(),
+ SdkConstants.FD_TOOLS);
+ File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName());
+
+ if (mkSdCard.isFile() == false) {
+ log.error(null, "'%1$s' is missing from the SDK tools folder.",
+ mkSdCard.getName());
+ needCleanup = true;
+ return null;
+ }
+
+ if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) {
+ needCleanup = true;
+ return null; // mksdcard output has already been displayed, no need to
+ // output anything else.
+ }
+
+ // add a property containing the size of the sdcard for display purpose
+ // only when the dev does 'android list avd'
+ values.put(AVD_INI_SDCARD_SIZE, sdcard);
+ } else {
+ log.error(null, "'%1$s' is not recognized as a valid sdcard value.\n"
+ + "Value should be:\n" + "1. path to an sdcard.\n"
+ + "2. size of the sdcard to create: <size>[K|M]", sdcard);
+ needCleanup = true;
+ return null;
+ }
+ }
+ }
+
+ // add the hardware config to the config file.
+ // priority order is:
+ // - values provided by the user
+ // - values provided by the skin
+ // - values provided by the target (add-on only).
+ // In order to follow this priority, we'll add the lowest priority values first and then
+ // override by higher priority values.
+ // In the case of a platform with override values from the user, the skin value might
+ // already be there, but it's ok.
+
+ HashMap<String, String> finalHardwareValues = new HashMap<String, String>();
+
+ FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(),
+ AvdManager.HARDWARE_INI);
+ if (targetHardwareFile.isFile()) {
+ Map<String, String> targetHardwareConfig = ProjectProperties.parsePropertyFile(
+ targetHardwareFile, log);
+
+ if (targetHardwareConfig != null) {
+ finalHardwareValues.putAll(targetHardwareConfig);
+ values.putAll(targetHardwareConfig);
+ }
+ }
+
+ // get the hardware properties for this skin
+ File skinFolder = getSkinPath(skinName, target);
+ FileWrapper skinHardwareFile = new FileWrapper(skinFolder, AvdManager.HARDWARE_INI);
+ if (skinHardwareFile.isFile()) {
+ Map<String, String> skinHardwareConfig = ProjectProperties.parsePropertyFile(
+ skinHardwareFile, log);
+
+ if (skinHardwareConfig != null) {
+ finalHardwareValues.putAll(skinHardwareConfig);
+ values.putAll(skinHardwareConfig);
+ }
+ }
+
+ // finally put the hardware provided by the user.
+ if (hardwareConfig != null) {
+ finalHardwareValues.putAll(hardwareConfig);
+ values.putAll(hardwareConfig);
+ }
+
+ File configIniFile = new File(avdFolder, CONFIG_INI);
+ writeIniFile(configIniFile, values);
+
+ // Generate the log report first because we want to control where line breaks
+ // are located when generating the hardware config list.
+ StringBuilder report = new StringBuilder();
+
+ if (target.isPlatform()) {
+ report.append(String.format("Created AVD '%1$s' based on %2$s",
+ name, target.getName()));
+ } else {
+ report.append(String.format("Created AVD '%1$s' based on %2$s (%3$s)", name,
+ target.getName(), target.getVendor()));
+ }
+
+ // display the chosen hardware config
+ if (finalHardwareValues.size() > 0) {
+ report.append(",\nwith the following hardware config:\n");
+ for (Entry<String, String> entry : finalHardwareValues.entrySet()) {
+ report.append(String.format("%s=%s\n",entry.getKey(), entry.getValue()));
+ }
+ } else {
+ report.append("\n");
+ }
+
+ log.printf(report.toString());
+
+ // create the AvdInfo object, and add it to the list
+ AvdInfo newAvdInfo = new AvdInfo(name,
+ avdFolder.getAbsolutePath(),
+ target.hashString(),
+ target, values);
+
+ AvdInfo oldAvdInfo = getAvd(name, false /*validAvdOnly*/);
+
+ synchronized (mAllAvdList) {
+ if (oldAvdInfo != null && removePrevious) {
+ mAllAvdList.remove(oldAvdInfo);
+ }
+ mAllAvdList.add(newAvdInfo);
+ mValidAvdList = mBrokenAvdList = null;
+ }
+
+ if (removePrevious &&
+ newAvdInfo != null &&
+ oldAvdInfo != null &&
+ !oldAvdInfo.getPath().equals(newAvdInfo.getPath())) {
+ log.warning("Removing previous AVD directory at %s", oldAvdInfo.getPath());
+ // Remove the old data directory
+ File dir = new File(oldAvdInfo.getPath());
+ try {
+ deleteContentOf(dir);
+ dir.delete();
+ } catch (SecurityException e) {
+ log.error(e, "Failed to delete %1$s", dir.getAbsolutePath());
+ }
+ }
+
+ return newAvdInfo;
+ } catch (AndroidLocationException e) {
+ log.error(e, null);
+ } catch (IOException e) {
+ log.error(e, null);
+ } catch (SecurityException e) {
+ log.error(e, null);
+ } finally {
+ if (needCleanup) {
+ if (iniFile != null && iniFile.exists()) {
+ iniFile.delete();
+ }
+
+ try {
+ deleteContentOf(avdFolder);
+ avdFolder.delete();
+ } catch (SecurityException e) {
+ log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath());
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Copy the nominated file to the given destination.
+ * @param source
+ * @param destination
+ *
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ private void copyImageFile(File source, File destination)
+ throws FileNotFoundException, IOException {
+ FileInputStream fis = new FileInputStream(source);
+ FileOutputStream fos = new FileOutputStream(destination);
+
+ byte[] buffer = new byte[4096];
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+
+ fos.close();
+ fis.close();
+ }
+
+ /**
+ * Returns the path to the target images folder as a relative path to the SDK, if the folder
+ * is not empty. If the image folder is empty or does not exist, <code>null</code> is returned.
+ * @throws InvalidTargetPathException if the target image folder is not in the current SDK.
+ */
+ private String getImageRelativePath(IAndroidTarget target)
+ throws InvalidTargetPathException {
+ String imageFullPath = target.getPath(IAndroidTarget.IMAGES);
+
+ // make this path relative to the SDK location
+ String sdkLocation = mSdkManager.getLocation();
+ if (imageFullPath.startsWith(sdkLocation) == false) {
+ // this really really should not happen.
+ assert false;
+ throw new InvalidTargetPathException("Target location is not inside the SDK.");
+ }
+
+ File folder = new File(imageFullPath);
+ if (folder.isDirectory()) {
+ String[] list = folder.list(new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return IMAGE_NAME_PATTERN.matcher(name).matches();
+ }
+ });
+
+ if (list.length > 0) {
+ imageFullPath = imageFullPath.substring(sdkLocation.length());
+ if (imageFullPath.charAt(0) == File.separatorChar) {
+ imageFullPath = imageFullPath.substring(1);
+ }
+
+ return imageFullPath;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the path to the skin, as a relative path to the SDK.
+ * @param skinName The name of the skin to find. Case-sensitive.
+ * @param target The target where to find the skin.
+ * @param log the log object to receive action logs. Cannot be null.
+ */
+ public String getSkinRelativePath(String skinName, IAndroidTarget target, ISdkLog log) {
+ if (log == null) {
+ throw new IllegalArgumentException("log cannot be null");
+ }
+
+ // first look to see if the skin is in the target
+ File skin = getSkinPath(skinName, target);
+
+ // skin really does not exist!
+ if (skin.exists() == false) {
+ log.error(null, "Skin '%1$s' does not exist.", skinName);
+ return null;
+ }
+
+ // get the skin path
+ String path = skin.getAbsolutePath();
+
+ // make this path relative to the SDK location
+ String sdkLocation = mSdkManager.getLocation();
+ if (path.startsWith(sdkLocation) == false) {
+ // this really really should not happen.
+ log.error(null, "Target location is not inside the SDK.");
+ assert false;
+ return null;
+ }
+
+ path = path.substring(sdkLocation.length());
+ if (path.charAt(0) == File.separatorChar) {
+ path = path.substring(1);
+ }
+ return path;
+ }
+
+ /**
+ * Returns the full absolute OS path to a skin specified by name for a given target.
+ * @param skinName The name of the skin to find. Case-sensitive.
+ * @param target The target where to find the skin.
+ * @return a {@link File} that may or may not actually exist.
+ */
+ public File getSkinPath(String skinName, IAndroidTarget target) {
+ String path = target.getPath(IAndroidTarget.SKINS);
+ File skin = new File(path, skinName);
+
+ if (skin.exists() == false && target.isPlatform() == false) {
+ target = target.getParent();
+
+ path = target.getPath(IAndroidTarget.SKINS);
+ skin = new File(path, skinName);
+ }
+
+ return skin;
+ }
+
+ /**
+ * Creates the ini file for an AVD.
+ *
+ * @param name of the AVD.
+ * @param avdFolder path for the data folder of the AVD.
+ * @param target of the AVD.
+ * @throws AndroidLocationException if there's a problem getting android root directory.
+ * @throws IOException if {@link File#getAbsolutePath()} fails.
+ */
+ private File createAvdIniFile(String name, File avdFolder, IAndroidTarget target)
+ throws AndroidLocationException, IOException {
+ HashMap<String, String> values = new HashMap<String, String>();
+ File iniFile = AvdInfo.getIniFile(name);
+ values.put(AVD_INFO_PATH, avdFolder.getAbsolutePath());
+ values.put(AVD_INFO_TARGET, target.hashString());
+ writeIniFile(iniFile, values);
+
+ return iniFile;
+ }
+
+ /**
+ * Creates the ini file for an AVD.
+ *
+ * @param info of the AVD.
+ * @throws AndroidLocationException if there's a problem getting android root directory.
+ * @throws IOException if {@link File#getAbsolutePath()} fails.
+ */
+ private File createAvdIniFile(AvdInfo info) throws AndroidLocationException, IOException {
+ return createAvdIniFile(info.getName(), new File(info.getPath()), info.getTarget());
+ }
+
+ /**
+ * Actually deletes the files of an existing AVD.
+ * <p/>
+ * This also remove it from the manager's list, The caller does not need to
+ * call {@link #removeAvd(AvdInfo)} afterwards.
+ * <p/>
+ * This method is designed to somehow work with an unavailable AVD, that is an AVD that
+ * could not be loaded due to some error. That means this method still tries to remove
+ * the AVD ini file or its folder if it can be found. An error will be output if any of
+ * these operations fail.
+ *
+ * @param avdInfo the information on the AVD to delete
+ * @param log the log object to receive action logs. Cannot be null.
+ * @return True if the AVD was deleted with no error.
+ */
+ public boolean deleteAvd(AvdInfo avdInfo, ISdkLog log) {
+ try {
+ boolean error = false;
+
+ File f = avdInfo.getIniFile();
+ if (f != null && f.exists()) {
+ log.printf("Deleting file %1$s\n", f.getCanonicalPath());
+ if (!f.delete()) {
+ log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath());
+ error = true;
+ }
+ }
+
+ String path = avdInfo.getPath();
+ if (path != null) {
+ f = new File(path);
+ if (f.exists()) {
+ log.printf("Deleting folder %1$s\n", f.getCanonicalPath());
+ if (deleteContentOf(f) == false || f.delete() == false) {
+ log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath());
+ error = true;
+ }
+ }
+ }
+
+ removeAvd(avdInfo);
+
+ if (error) {
+ log.printf("\nAVD '%1$s' deleted with errors. See errors above.\n",
+ avdInfo.getName());
+ } else {
+ log.printf("\nAVD '%1$s' deleted.\n", avdInfo.getName());
+ return true;
+ }
+
+ } catch (AndroidLocationException e) {
+ log.error(e, null);
+ } catch (IOException e) {
+ log.error(e, null);
+ } catch (SecurityException e) {
+ log.error(e, null);
+ }
+ return false;
+ }
+
+ /**
+ * Moves and/or rename an existing AVD and its files.
+ * This also change it in the manager's list.
+ * <p/>
+ * The caller should make sure the name or path given are valid, do not exist and are
+ * actually different than current values.
+ *
+ * @param avdInfo the information on the AVD to move.
+ * @param newName the new name of the AVD if non null.
+ * @param paramFolderPath the new data folder if non null.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @return True if the move succeeded or there was nothing to do.
+ * If false, this method will have had already output error in the log.
+ */
+ public boolean moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log) {
+
+ try {
+ if (paramFolderPath != null) {
+ File f = new File(avdInfo.getPath());
+ log.warning("Moving '%1$s' to '%2$s'.", avdInfo.getPath(), paramFolderPath);
+ if (!f.renameTo(new File(paramFolderPath))) {
+ log.error(null, "Failed to move '%1$s' to '%2$s'.",
+ avdInfo.getPath(), paramFolderPath);
+ return false;
+ }
+
+ // update AVD info
+ AvdInfo info = new AvdInfo(avdInfo.getName(), paramFolderPath,
+ avdInfo.getTargetHash(), avdInfo.getTarget(), avdInfo.getProperties());
+ replaceAvd(avdInfo, info);
+
+ // update the ini file
+ createAvdIniFile(info);
+ }
+
+ if (newName != null) {
+ File oldIniFile = avdInfo.getIniFile();
+ File newIniFile = AvdInfo.getIniFile(newName);
+
+ log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath());
+ if (!oldIniFile.renameTo(newIniFile)) {
+ log.error(null, "Failed to move '%1$s' to '%2$s'.",
+ oldIniFile.getPath(), newIniFile.getPath());
+ return false;
+ }
+
+ // update AVD info
+ AvdInfo info = new AvdInfo(newName, avdInfo.getPath(),
+ avdInfo.getTargetHash(), avdInfo.getTarget(), avdInfo.getProperties());
+ replaceAvd(avdInfo, info);
+ }
+
+ log.printf("AVD '%1$s' moved.\n", avdInfo.getName());
+
+ } catch (AndroidLocationException e) {
+ log.error(e, null);
+ } catch (IOException e) {
+ log.error(e, null);
+ }
+
+ // nothing to do or succeeded
+ return true;
+ }
+
+ /**
+ * Helper method to recursively delete a folder's content (but not the folder itself).
+ *
+ * @throws SecurityException like {@link File#delete()} does if file/folder is not writable.
+ */
+ private boolean deleteContentOf(File folder) throws SecurityException {
+ File[] files = folder.listFiles();
+ if (files != null) {
+ for (File f : files) {
+ if (f.isDirectory()) {
+ if (deleteContentOf(f) == false) {
+ return false;
+ }
+ }
+ if (f.delete() == false) {
+ return false;
+ }
+
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a list of files that are potential AVD ini files.
+ * <p/>
+ * This lists the $HOME/.android/avd/<name>.ini files.
+ * Such files are properties file than then indicate where the AVD folder is located.
+ *
+ * @return A new {@link File} array or null. The array might be empty.
+ * @throws AndroidLocationException if there's a problem getting android root directory.
+ */
+ private File[] buildAvdFilesList() throws AndroidLocationException {
+ // get the Android prefs location.
+ String avdRoot = AvdManager.getBaseAvdFolder();
+
+ // ensure folder validity.
+ File folder = new File(avdRoot);
+ if (folder.isFile()) {
+ throw new AndroidLocationException(
+ String.format("%1$s is not a valid folder.", avdRoot));
+ } else if (folder.exists() == false) {
+ // folder is not there, we create it and return
+ folder.mkdirs();
+ return null;
+ }
+
+ File[] avds = folder.listFiles(new FilenameFilter() {
+ public boolean accept(File parent, String name) {
+ if (INI_NAME_PATTERN.matcher(name).matches()) {
+ // check it's a file and not a folder
+ boolean isFile = new File(parent, name).isFile();
+ return isFile;
+ }
+
+ return false;
+ }
+ });
+
+ return avds;
+ }
+
+ /**
+ * Computes the internal list of available AVDs
+ * @param allList the list to contain all the AVDs
+ * @param log the log object to receive action logs. Cannot be null.
+ *
+ * @throws AndroidLocationException if there's a problem getting android root directory.
+ */
+ private void buildAvdList(ArrayList<AvdInfo> allList, ISdkLog log)
+ throws AndroidLocationException {
+ File[] avds = buildAvdFilesList();
+ if (avds != null) {
+ for (File avd : avds) {
+ AvdInfo info = parseAvdInfo(avd, log);
+ if (info != null) {
+ allList.add(info);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses an AVD .ini file to create an {@link AvdInfo}.
+ *
+ * @param path The path to the AVD .ini file
+ * @param log the log object to receive action logs. Cannot be null.
+ * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is
+ * valid or not.
+ */
+ private AvdInfo parseAvdInfo(File path, ISdkLog log) {
+ Map<String, String> map = ProjectProperties.parsePropertyFile(
+ new FileWrapper(path),
+ log);
+
+ String avdPath = map.get(AVD_INFO_PATH);
+ String targetHash = map.get(AVD_INFO_TARGET);
+
+ IAndroidTarget target = null;
+ FileWrapper configIniFile = null;
+ Map<String, String> properties = null;
+
+ if (targetHash != null) {
+ target = mSdkManager.getTargetFromHashString(targetHash);
+ }
+
+ // load the AVD properties.
+ if (avdPath != null) {
+ configIniFile = new FileWrapper(avdPath, CONFIG_INI);
+ }
+
+ if (configIniFile != null) {
+ if (!configIniFile.isFile()) {
+ log.warning("Missing file '%1$s'.", configIniFile.getPath());
+ } else {
+ properties = ProjectProperties.parsePropertyFile(configIniFile, log);
+ }
+ }
+
+ // get name
+ String name = path.getName();
+ Matcher matcher = INI_NAME_PATTERN.matcher(path.getName());
+ if (matcher.matches()) {
+ name = matcher.group(1);
+ }
+
+ // check the image.sysdir are valid
+ boolean validImageSysdir = true;
+ if (properties != null) {
+ String imageSysDir = properties.get(AVD_INI_IMAGES_1);
+ if (imageSysDir != null) {
+ File f = new File(mSdkManager.getLocation() + File.separator + imageSysDir);
+ if (f.isDirectory() == false) {
+ validImageSysdir = false;
+ } else {
+ imageSysDir = properties.get(AVD_INI_IMAGES_2);
+ if (imageSysDir != null) {
+ f = new File(mSdkManager.getLocation() + File.separator + imageSysDir);
+ if (f.isDirectory() == false) {
+ validImageSysdir = false;
+ }
+ }
+ }
+ }
+ }
+
+ // TODO: What about missing sdcard, skins, etc?
+
+ AvdStatus status;
+
+ if (avdPath == null) {
+ status = AvdStatus.ERROR_PATH;
+ } else if (configIniFile == null) {
+ status = AvdStatus.ERROR_CONFIG;
+ } else if (targetHash == null) {
+ status = AvdStatus.ERROR_TARGET_HASH;
+ } else if (target == null) {
+ status = AvdStatus.ERROR_TARGET;
+ } else if (properties == null) {
+ status = AvdStatus.ERROR_PROPERTIES;
+ } else if (validImageSysdir == false) {
+ status = AvdStatus.ERROR_IMAGE_DIR;
+ } else {
+ status = AvdStatus.OK;
+ }
+
+ AvdInfo info = new AvdInfo(
+ name,
+ avdPath,
+ targetHash,
+ target,
+ properties,
+ status);
+
+ return info;
+ }
+
+ /**
+ * Writes a .ini file from a set of properties, using UTF-8 encoding.
+ *
+ * @param iniFile The file to generate.
+ * @param values THe properties to place in the ini file.
+ * @throws IOException if {@link FileWriter} fails to open, write or close the file.
+ */
+ private static void writeIniFile(File iniFile, Map<String, String> values)
+ throws IOException {
+ OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile),
+ SdkConstants.INI_CHARSET);
+
+ for (Entry<String, String> entry : values.entrySet()) {
+ writer.write(String.format("%1$s=%2$s\n", entry.getKey(), entry.getValue()));
+ }
+ writer.close();
+ }
+
+ /**
+ * Invokes the tool to create a new SD card image file.
+ *
+ * @param toolLocation The path to the mksdcard tool.
+ * @param size The size of the new SD Card, compatible with {@link #SDCARD_SIZE_PATTERN}.
+ * @param location The path of the new sdcard image file to generate.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @return True if the sdcard could be created.
+ */
+ private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) {
+ try {
+ String[] command = new String[3];
+ command[0] = toolLocation;
+ command[1] = size;
+ command[2] = location;
+ Process process = Runtime.getRuntime().exec(command);
+
+ ArrayList<String> errorOutput = new ArrayList<String>();
+ ArrayList<String> stdOutput = new ArrayList<String>();
+ int status = grabProcessOutput(process, errorOutput, stdOutput,
+ true /* waitForReaders */);
+
+ if (status == 0) {
+ return true;
+ } else {
+ for (String error : errorOutput) {
+ log.error(null, error);
+ }
+ }
+
+ } catch (InterruptedException e) {
+ // pass, print error below
+ } catch (IOException e) {
+ // pass, print error below
+ }
+
+ log.error(null, "Failed to create the SD card.");
+ return false;
+ }
+
+ /**
+ * Gets the stderr/stdout outputs of a process and returns when the process is done.
+ * Both <b>must</b> be read or the process will block on windows.
+ * @param process The process to get the ouput from
+ * @param errorOutput The array to store the stderr output. cannot be null.
+ * @param stdOutput The array to store the stdout output. cannot be null.
+ * @param waitforReaders if true, this will wait for the reader threads.
+ * @return the process return code.
+ * @throws InterruptedException
+ */
+ private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput,
+ final ArrayList<String> stdOutput, boolean waitforReaders)
+ throws InterruptedException {
+ assert errorOutput != null;
+ assert stdOutput != null;
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ Thread t1 = new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ errorOutput.add(line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ };
+
+ Thread t2 = new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ stdOutput.add(line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ };
+
+ t1.start();
+ t2.start();
+
+ // it looks like on windows process#waitFor() can return
+ // before the thread have filled the arrays, so we wait for both threads and the
+ // process itself.
+ if (waitforReaders) {
+ try {
+ t1.join();
+ } catch (InterruptedException e) {
+ // nothing to do here
+ }
+ try {
+ t2.join();
+ } catch (InterruptedException e) {
+ // nothing to do here
+ }
+ }
+
+ // get the return code from the process
+ return process.waitFor();
+ }
+
+ /**
+ * Removes an {@link AvdInfo} from the internal list.
+ *
+ * @param avdInfo The {@link AvdInfo} to remove.
+ * @return true if this {@link AvdInfo} was present and has been removed.
+ */
+ public boolean removeAvd(AvdInfo avdInfo) {
+ synchronized (mAllAvdList) {
+ if (mAllAvdList.remove(avdInfo)) {
+ mValidAvdList = mBrokenAvdList = null;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Updates an AVD with new path to the system image folders.
+ * @param name the name of the AVD to update.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @throws IOException
+ */
+ public void updateAvd(String name, ISdkLog log) throws IOException {
+ // find the AVD to update. It should be be in the broken list.
+ AvdInfo avd = null;
+ synchronized (mAllAvdList) {
+ for (AvdInfo info : mAllAvdList) {
+ if (info.getName().equals(name)) {
+ avd = info;
+ break;
+ }
+ }
+ }
+
+ if (avd == null) {
+ // not in the broken list, just return.
+ log.error(null, "There is no Android Virtual Device named '%s'.", name);
+ return;
+ }
+
+ updateAvd(avd, log);
+ }
+
+
+ /**
+ * Updates an AVD with new path to the system image folders.
+ * @param avd the AVD to update.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @throws IOException
+ */
+ public void updateAvd(AvdInfo avd, ISdkLog log) throws IOException {
+ // get the properties. This is a unmodifiable Map.
+ Map<String, String> oldProperties = avd.getProperties();
+
+ // create a new map
+ Map<String, String> properties = new HashMap<String, String>();
+ if (oldProperties != null) {
+ properties.putAll(oldProperties);
+ }
+
+ AvdStatus status;
+
+ // create the path to the new system images.
+ if (setImagePathProperties(avd.getTarget(), properties, log)) {
+ if (properties.containsKey(AVD_INI_IMAGES_1)) {
+ log.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_1,
+ properties.get(AVD_INI_IMAGES_1));
+ }
+
+ if (properties.containsKey(AVD_INI_IMAGES_2)) {
+ log.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_2,
+ properties.get(AVD_INI_IMAGES_2));
+ }
+
+ status = AvdStatus.OK;
+ } else {
+ log.error(null, "Unable to find non empty system images folders for %1$s",
+ avd.getName());
+ //FIXME: display paths to empty image folders?
+ status = AvdStatus.ERROR_IMAGE_DIR;
+ }
+
+ // now write the config file
+ File configIniFile = new File(avd.getPath(), CONFIG_INI);
+ writeIniFile(configIniFile, properties);
+
+ // finally create a new AvdInfo for this unbroken avd and add it to the list.
+ // instead of creating the AvdInfo object directly we reparse it, to detect other possible
+ // errors
+ // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect other errors.
+ AvdInfo newAvd = new AvdInfo(
+ avd.getName(),
+ avd.getPath(),
+ avd.getTargetHash(),
+ avd.getTarget(),
+ properties,
+ status);
+
+ replaceAvd(avd, newAvd);
+ }
+
+ /**
+ * Sets the paths to the system images in a properties map.
+ * @param target the target in which to find the system images.
+ * @param properties the properties in which to set the paths.
+ * @param log the log object to receive action logs. Cannot be null.
+ * @return true if success, false if some path are missing.
+ */
+ private boolean setImagePathProperties(IAndroidTarget target,
+ Map<String, String> properties,
+ ISdkLog log) {
+ properties.remove(AVD_INI_IMAGES_1);
+ properties.remove(AVD_INI_IMAGES_2);
+
+ try {
+ String property = AVD_INI_IMAGES_1;
+
+ // First the image folders of the target itself
+ String imagePath = getImageRelativePath(target);
+ if (imagePath != null) {
+ properties.put(property, imagePath);
+ property = AVD_INI_IMAGES_2;
+ }
+
+
+ // If the target is an add-on we need to add the Platform image as a backup.
+ IAndroidTarget parent = target.getParent();
+ if (parent != null) {
+ imagePath = getImageRelativePath(parent);
+ if (imagePath != null) {
+ properties.put(property, imagePath);
+ }
+ }
+
+ // we need at least one path!
+ return properties.containsKey(AVD_INI_IMAGES_1);
+ } catch (InvalidTargetPathException e) {
+ log.error(e, e.getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Replaces an old {@link AvdInfo} with a new one in the lists storing them.
+ * @param oldAvd the {@link AvdInfo} to remove.
+ * @param newAvd the {@link AvdInfo} to add.
+ */
+ private void replaceAvd(AvdInfo oldAvd, AvdInfo newAvd) {
+ synchronized (mAllAvdList) {
+ mAllAvdList.remove(oldAvd);
+ mAllAvdList.add(newAvd);
+ mValidAvdList = mBrokenAvdList = null;
+ }
+ }
+}