| /* |
| * 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.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.concurrency.Slow; |
| import com.android.io.CancellableFileIo; |
| import com.android.io.IAbstractFile; |
| import com.android.io.StreamException; |
| import com.android.prefs.AbstractAndroidLocations; |
| import com.android.prefs.AndroidLocationsException; |
| import com.android.prefs.AndroidLocationsSingleton; |
| import com.android.repository.api.ConsoleProgressIndicator; |
| import com.android.repository.api.LocalPackage; |
| import com.android.repository.api.ProgressIndicator; |
| import com.android.repository.io.FileOp; |
| import com.android.repository.io.FileOpUtils; |
| import com.android.sdklib.AndroidTargetHash; |
| import com.android.sdklib.AndroidVersion; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.ISystemImage; |
| import com.android.sdklib.PathFileWrapper; |
| import com.android.sdklib.devices.Abi; |
| import com.android.sdklib.devices.Device; |
| import com.android.sdklib.devices.DeviceManager; |
| import com.android.sdklib.devices.DeviceManager.DeviceStatus; |
| import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; |
| import com.android.sdklib.internal.project.ProjectProperties; |
| import com.android.sdklib.repository.AndroidSdkHandler; |
| import com.android.sdklib.repository.IdDisplay; |
| import com.android.sdklib.repository.LoggerProgressIndicatorWrapper; |
| import com.android.sdklib.repository.targets.SystemImage; |
| import com.android.utils.GrabProcessOutput; |
| import com.android.utils.GrabProcessOutput.IProcessOutput; |
| import com.android.utils.GrabProcessOutput.Wait; |
| import com.android.utils.ILogger; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.HashBasedTable; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Table; |
| import com.google.common.io.Closeables; |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.lang.ref.WeakReference; |
| import java.nio.charset.Charset; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Stream; |
| |
| /** |
| * Android Virtual Device Manager to manage AVDs. |
| */ |
| public class AvdManager { |
| |
| private File mBaseAvdFolder; |
| |
| /** |
| * Exception thrown when something is wrong with a target path. |
| */ |
| private static final class InvalidTargetPathException extends Exception { |
| private static final long serialVersionUID = 1L; |
| |
| InvalidTargetPathException(String message) { |
| super(message); |
| } |
| } |
| |
| private static final Pattern INI_LINE_PATTERN = |
| Pattern.compile("^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); //$NON-NLS-1$ |
| |
| public static final String AVD_FOLDER_EXTENSION = ".avd"; //$NON-NLS-1$ |
| |
| /** Charset encoding used by the avd.ini/config.ini. */ |
| public static final String AVD_INI_ENCODING = "avd.ini.encoding"; //$NON-NLS-1$ |
| |
| /** |
| * The *absolute* path to the AVD folder (which contains the #CONFIG_INI file). |
| */ |
| public static final String AVD_INFO_ABS_PATH = "path"; //$NON-NLS-1$ |
| |
| /** |
| * The path to the AVD folder (which contains the #CONFIG_INI file) relative to the {@link |
| * AbstractAndroidLocations#FOLDER_DOT_ANDROID}. This information is written in the avd ini |
| * <b>only</b> if the AVD folder is located under the .android path (that is the relative that |
| * has no backward {@code ..} references). |
| */ |
| public static final String AVD_INFO_REL_PATH = "path.rel"; // $NON-NLS-1$ |
| |
| /** |
| * The {@link IAndroidTarget#hashString()} of the AVD. |
| */ |
| public static final String AVD_INFO_TARGET = "target"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the tag id of the specific avd |
| */ |
| public static final String AVD_INI_TAG_ID = "tag.id"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the tag display of the specific avd |
| */ |
| public static final String AVD_INI_TAG_DISPLAY = "tag.display"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the abi type of the specific avd |
| */ |
| public static final String AVD_INI_ABI_TYPE = "abi.type"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the name of the AVD |
| */ |
| public static final String AVD_INI_AVD_ID = "AvdId"; |
| |
| /** |
| * AVD/config.ini key name representing the name of the AVD |
| */ |
| public static final String AVD_INI_PLAYSTORE_ENABLED = "PlayStore.enabled"; |
| |
| /** |
| * AVD/config.ini key name representing the CPU architecture of the specific avd |
| */ |
| public static final String AVD_INI_CPU_ARCH = "hw.cpu.arch"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the CPU architecture of the specific avd |
| */ |
| public static final String AVD_INI_CPU_MODEL = "hw.cpu.model"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the number of processors to emulate when SMP is supported. |
| */ |
| public static final String AVD_INI_CPU_CORES = "hw.cpu.ncore"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the manufacturer of the device this avd was based on. |
| */ |
| public static final String AVD_INI_DEVICE_MANUFACTURER = "hw.device.manufacturer"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the name of the device this avd was based on. |
| */ |
| public static final String AVD_INI_DEVICE_NAME = "hw.device.name"; //$NON-NLS-1$ |
| |
| /** AVD/config.ini key name representing if it's Chrome OS (App Runtime for Chrome). */ |
| public static final String AVD_INI_ARC = "hw.arc"; |
| |
| /** |
| * AVD/config.ini key name representing the display name of the AVD |
| */ |
| public static final String AVD_INI_DISPLAY_NAME = "avd.ini.displayname"; |
| |
| /** |
| * 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 static final String AVD_INI_SKIN_PATH = "skin.path"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the SDK-relative path of the skin folder to be selected if |
| * skins for this device become enabled. |
| */ |
| public static final String AVD_INI_BACKUP_SKIN_PATH = "skin.path.backup"; //$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 static final String AVD_INI_SKIN_NAME = "skin.name"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing whether a dynamic skin should be displayed. |
| */ |
| public static final String AVD_INI_SKIN_DYNAMIC = "skin.dynamic"; //$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 static final 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 |
| * @see #parseSdcardSize(String, String[]) |
| */ |
| public static final 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 static final 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 static final 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. |
| */ |
| public static final String AVD_INI_SNAPSHOT_PRESENT = "snapshot.present"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing whether hardware OpenGLES emulation is enabled |
| */ |
| public static final String AVD_INI_GPU_EMULATION = "hw.gpu.enabled"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing which software OpenGLES should be used |
| */ |
| public static final String AVD_INI_GPU_MODE = "hw.gpu.mode"; |
| |
| /** |
| * AVD/config.ini key name representing whether to boot from a snapshot |
| */ |
| public static final String AVD_INI_FORCE_COLD_BOOT_MODE = "fastboot.forceColdBoot"; |
| public static final String AVD_INI_FORCE_CHOSEN_SNAPSHOT_BOOT_MODE = "fastboot.forceChosenSnapshotBoot"; |
| public static final String AVD_INI_FORCE_FAST_BOOT_MODE = "fastboot.forceFastBoot"; |
| public static final String AVD_INI_CHOSEN_SNAPSHOT_FILE = "fastboot.chosenSnapshotFile"; |
| |
| /** |
| * AVD/config.ini key name representing how to emulate the front facing camera |
| */ |
| public static final String AVD_INI_CAMERA_FRONT = "hw.camera.front"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing how to emulate the rear facing camera |
| */ |
| public static final String AVD_INI_CAMERA_BACK = "hw.camera.back"; //$NON-NLS-1$ |
| |
| /** |
| * AVD/config.ini key name representing the amount of RAM the emulated device should have |
| */ |
| public static final String AVD_INI_RAM_SIZE = "hw.ramSize"; |
| |
| /** |
| * AVD/config.ini key name representing the amount of memory available to applications by default |
| */ |
| public static final String AVD_INI_VM_HEAP_SIZE = "vm.heapSize"; |
| |
| /** |
| * AVD/config.ini key name representing the size of the data partition |
| */ |
| public static final String AVD_INI_DATA_PARTITION_SIZE = "disk.dataPartition.size"; |
| |
| /** |
| * AVD/config.ini key name representing the hash of the device this AVD is based on. <br> |
| * This old hash is deprecated and shouldn't be used anymore. |
| * It represents the Device.hashCode() and is not stable accross implementations. |
| * @see #AVD_INI_DEVICE_HASH_V2 |
| */ |
| public static final String AVD_INI_DEVICE_HASH_V1 = "hw.device.hash"; |
| |
| /** |
| * AVD/config.ini key name representing the hash of the device hardware properties |
| * actually present in the config.ini. This replaces {@link #AVD_INI_DEVICE_HASH_V1}. |
| * <p> |
| * To find this hash, use |
| * {@code DeviceManager.getHardwareProperties(device).get(AVD_INI_DEVICE_HASH_V2)}. |
| */ |
| public static final String AVD_INI_DEVICE_HASH_V2 = "hw.device.hash2"; |
| |
| /** AVD/config.ini key name representing the Android display settings file */ |
| public static final String AVD_INI_DISPLAY_SETTINGS_FILE = "display.settings.xml"; |
| |
| /** AVD/config.ini key name representing the hinge settings */ |
| public static final String AVD_INI_HINGE = "hw.sensor.hinge"; |
| |
| public static final String AVD_INI_HINGE_COUNT = "hw.sensor.hinge.count"; |
| public static final String AVD_INI_HINGE_TYPE = "hw.sensor.hinge.type"; |
| public static final String AVD_INI_HINGE_SUB_TYPE = "hw.sensor.hinge.sub_type"; |
| public static final String AVD_INI_HINGE_RANGES = "hw.sensor.hinge.ranges"; |
| public static final String AVD_INI_HINGE_DEFAULTS = "hw.sensor.hinge.defaults"; |
| public static final String AVD_INI_HINGE_AREAS = "hw.sensor.hinge.areas"; |
| public static final String AVD_INI_POSTURE_LISTS = "hw.sensor.posture_list"; |
| public static final String AVD_INI_FOLD_AT_POSTURE = "hw.sensor.hinge.fold_to_displayRegion.0.1_at_posture"; |
| public static final String AVD_INI_HINGE_ANGLES_POSTURE_DEFINITIONS = |
| "hw.sensor.hinge_angles_posture_definitions"; |
| |
| /** AVD/config.ini key name representing the rollable settings */ |
| public static final String AVD_INI_ROLL = "hw.sensor.roll"; |
| |
| public static final String AVD_INI_ROLL_COUNT = "hw.sensor.roll.count"; |
| public static final String AVD_INI_ROLL_RANGES = "hw.sensor.roll.ranges"; |
| public static final String AVD_INI_ROLL_DEFAULTS = "hw.sensor.roll.defaults"; |
| public static final String AVD_INI_ROLL_RADIUS = "hw.sensor.roll.radius"; |
| public static final String AVD_INI_ROLL_DIRECTION = "hw.sensor.roll.direction"; |
| public static final String AVD_INI_ROLL_RESIZE_1_AT_POSTURE = |
| "hw.sensor.roll.resize_to_displayRegion.0.1_at_posture"; |
| public static final String AVD_INI_ROLL_RESIZE_2_AT_POSTURE = |
| "hw.sensor.roll.resize_to_displayRegion.0.2_at_posture"; |
| public static final String AVD_INI_ROLL_RESIZE_3_AT_POSTURE = |
| "hw.sensor.roll.resize_to_displayRegion.0.3_at_posture"; |
| public static final String AVD_INI_ROLL_PERCENTAGES_POSTURE_DEFINITIONS = |
| "hw.sensor.roll_percentages_posture_definitions"; |
| |
| /** |
| * The API level of this AVD. Derived from the target hash. |
| */ |
| public static final String AVD_INI_ANDROID_API = "image.androidVersion.api"; |
| |
| /** |
| * The API codename of this AVD. Derived from the target hash. |
| */ |
| public static final String AVD_INI_ANDROID_CODENAME = "image.androidVersion.codename"; |
| |
| /** |
| * Pattern to match pixel-sized skin "names", e.g. "320x480". |
| */ |
| public static final Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$ |
| public static final String DATA_FOLDER = "data"; |
| public static final String USERDATA_IMG = "userdata.img"; |
| public static final String USERDATA_QEMU_IMG = "userdata-qemu.img"; |
| public static final String SNAPSHOTS_DIRECTORY = "snapshots"; |
| |
| private static final String BOOT_PROP = "boot.prop"; //$NON-NLS-1$ |
| static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$ |
| private static final String HARDWARE_QEMU_INI = "hardware-qemu.ini"; |
| private static final String SDCARD_IMG = "sdcard.img"; //$NON-NLS-1$ |
| |
| static final String INI_EXTENSION = ".ini"; //$NON-NLS-1$ |
| private static final Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + //$NON-NLS-1$ |
| INI_EXTENSION + "$", //$NON-NLS-1$ |
| Pattern.CASE_INSENSITIVE); |
| |
| private static final Pattern IMAGE_NAME_PATTERN = Pattern.compile("(.+)\\.img$", //$NON-NLS-1$ |
| Pattern.CASE_INSENSITIVE); |
| |
| /** |
| * Pattern for matching SD Card sizes, e.g. "4K" or "16M". |
| * Callers should use {@link #parseSdcardSize(String, String[])} instead of using this directly. |
| */ |
| private static final Pattern SDCARD_SIZE_PATTERN = Pattern.compile("(\\d+)([KMG])"); //$NON-NLS-1$ |
| |
| /** |
| * Minimal size of an SDCard image file in bytes. Currently 9 MiB. |
| */ |
| |
| public static final long SDCARD_MIN_BYTE_SIZE = 9<<20; |
| /** |
| * Maximal size of an SDCard image file in bytes. Currently 1023 GiB. |
| */ |
| public static final long SDCARD_MAX_BYTE_SIZE = 1023L<<30; |
| |
| /** The sdcard string represents a valid number but the size is outside of the allowed range. */ |
| public static final int SDCARD_SIZE_NOT_IN_RANGE = 0; |
| /** The sdcard string looks like a size number+suffix but the number failed to decode. */ |
| public static final int SDCARD_SIZE_INVALID = -1; |
| /** The sdcard string doesn't look like a size, it might be a path instead. */ |
| public static final int SDCARD_NOT_SIZE_PATTERN = -2; |
| |
| public static final String HARDWARE_INI = "hardware.ini"; //$NON-NLS-1$ |
| |
| private class AvdMgrException extends Exception { }; |
| |
| // A map where the keys are the locations of the SDK and the values are the corresponding |
| // AvdManagers. This prevents us from creating multiple AvdManagers for the same SDK and having |
| // them get out of sync. |
| private static final Table<String, FileOp, WeakReference<AvdManager>> mManagers = |
| HashBasedTable.create(); |
| |
| private final ArrayList<AvdInfo> mAllAvdList = new ArrayList<>(); |
| private AvdInfo[] mValidAvdList; |
| private AvdInfo[] mBrokenAvdList; |
| private final AndroidSdkHandler mSdkHandler; |
| private final Map<ILogger, DeviceManager> mDeviceManagers = |
| new HashMap<>(); |
| private final FileOp mFop; |
| |
| protected AvdManager( |
| @NonNull AndroidSdkHandler sdkHandler, |
| @NonNull File baseAvdFolder, |
| @NonNull ILogger log, |
| @NonNull FileOp fop) |
| throws AndroidLocationsException { |
| mSdkHandler = sdkHandler; |
| mFop = fop; |
| mBaseAvdFolder = baseAvdFolder; |
| buildAvdList(mAllAvdList, log); |
| } |
| |
| /** |
| * Returns an AVD Manager for a given SDK represented by {@code sdkHandler}. One AVD Manager |
| * instance is created by SDK location and then cached and reused. |
| * |
| * @param sdkHandler The SDK handler. |
| * @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. |
| * @return The AVD Manager instance. |
| * @throws AndroidLocationsException if {@code sdkHandler} does not have a local path set. |
| */ |
| @Nullable |
| public static AvdManager getInstance( |
| @NonNull AndroidSdkHandler sdkHandler, @NonNull ILogger log) |
| throws AndroidLocationsException { |
| return getInstance( |
| sdkHandler, AndroidLocationsSingleton.INSTANCE.getAvdLocation().toFile(), log); |
| } |
| |
| @Nullable |
| public static AvdManager getInstance( |
| @NonNull AndroidSdkHandler sdkHandler, |
| @NonNull File baseAvdFolder, |
| @NonNull ILogger log) |
| throws AndroidLocationsException { |
| if (sdkHandler.getLocation() == null) { |
| throw new AndroidLocationsException("Local SDK path not set!"); |
| } |
| synchronized(mManagers) { |
| AvdManager manager; |
| FileOp fop = sdkHandler.getFileOp(); |
| WeakReference<AvdManager> ref = mManagers.get(sdkHandler.getLocation().toString(), fop); |
| if (ref != null && (manager = ref.get()) != null) { |
| return manager; |
| } |
| try { |
| manager = new AvdManager(sdkHandler, baseAvdFolder, log, fop); |
| } catch (AndroidLocationsException e) { |
| throw e; |
| } |
| catch (Exception e) { |
| log.warning("Exception during AvdManager initialization: %1$s", e); |
| return null; |
| } |
| mManagers.put(sdkHandler.getLocation().toString(), fop, new WeakReference<>(manager)); |
| return manager; |
| } |
| } |
| |
| /** Returns the base folder where AVDs are created. */ |
| @NonNull |
| public File getBaseAvdFolder() { |
| return mBaseAvdFolder; |
| } |
| |
| /** |
| * Parse the sdcard string to decode the size. |
| * Returns: |
| * <ul> |
| * <li> The size in bytes > 0 if the sdcard string is a valid size in the allowed range. |
| * <li> {@link #SDCARD_SIZE_NOT_IN_RANGE} (0) |
| * if the sdcard string is a valid size NOT in the allowed range. |
| * <li> {@link #SDCARD_SIZE_INVALID} (-1) |
| * if the sdcard string is number that fails to parse correctly. |
| * <li> {@link #SDCARD_NOT_SIZE_PATTERN} (-2) |
| * if the sdcard string is not a number, in which case it's probably a file path. |
| * </ul> |
| * |
| * @param sdcard The sdcard string, which can be a file path, a size string or something else. |
| * @param parsedStrings If non-null, an array of 2 strings. The first string will be |
| * filled with the parsed numeric size and the second one will be filled with the |
| * parsed suffix. This is filled even if the returned size is deemed out of range or |
| * failed to parse. The values are null if the sdcard is not a size pattern. |
| * @return A size in byte if > 0, or {@link #SDCARD_SIZE_NOT_IN_RANGE}, |
| * {@link #SDCARD_SIZE_INVALID} or {@link #SDCARD_NOT_SIZE_PATTERN} as error codes. |
| */ |
| public static long parseSdcardSize(@NonNull String sdcard, @Nullable String[] parsedStrings) { |
| |
| if (parsedStrings != null) { |
| assert parsedStrings.length == 2; |
| parsedStrings[0] = null; |
| parsedStrings[1] = null; |
| } |
| |
| Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard); |
| if (m.matches()) { |
| if (parsedStrings != null) { |
| assert parsedStrings.length == 2; |
| parsedStrings[0] = m.group(1); |
| parsedStrings[1] = m.group(2); |
| } |
| |
| // get the sdcard values for checks |
| try { |
| long sdcardSize = Long.parseLong(m.group(1)); |
| |
| String sdcardSizeModifier = m.group(2); |
| if ("K".equals(sdcardSizeModifier)) { //$NON-NLS-1$ |
| sdcardSize <<= 10; |
| } else if ("M".equals(sdcardSizeModifier)) { //$NON-NLS-1$ |
| sdcardSize <<= 20; |
| } else if ("G".equals(sdcardSizeModifier)) { //$NON-NLS-1$ |
| sdcardSize <<= 30; |
| } |
| |
| if (sdcardSize < SDCARD_MIN_BYTE_SIZE || |
| sdcardSize > SDCARD_MAX_BYTE_SIZE) { |
| return SDCARD_SIZE_NOT_IN_RANGE; |
| } |
| |
| return sdcardSize; |
| } catch (NumberFormatException e) { |
| // This could happen if the number is too large to fit in a long. |
| return SDCARD_SIZE_INVALID; |
| } |
| } |
| |
| return SDCARD_NOT_SIZE_PATTERN; |
| } |
| |
| /** |
| * Returns all the existing AVDs. |
| * @return a newly allocated array containing all the AVDs. |
| */ |
| @NonNull |
| public AvdInfo[] getAllAvds() { |
| synchronized (mAllAvdList) { |
| return mAllAvdList.toArray(new AvdInfo[0]); |
| } |
| } |
| |
| /** |
| * Returns all the valid AVDs. |
| * @return a newly allocated array containing all valid the AVDs. |
| */ |
| @NonNull |
| public AvdInfo[] getValidAvds() { |
| synchronized (mAllAvdList) { |
| if (mValidAvdList == null) { |
| ArrayList<AvdInfo> list = new ArrayList<>(); |
| for (AvdInfo avd : mAllAvdList) { |
| if (avd.getStatus() == AvdStatus.OK) { |
| list.add(avd); |
| } |
| } |
| |
| mValidAvdList = list.toArray(new AvdInfo[0]); |
| } |
| return mValidAvdList; |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| @Nullable |
| public AvdInfo getAvd(@Nullable 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; |
| } |
| |
| /** Returns whether an emulator is currently running the AVD. */ |
| @Slow |
| public boolean isAvdRunning(@NonNull AvdInfo info, @NonNull ILogger logger) { |
| String pid; |
| try { |
| pid = getAvdPid(info); |
| } |
| catch (IOException e) { |
| logger.error(e, "IOException while getting PID"); |
| // To be safe return true |
| return true; |
| } |
| if (pid != null) { |
| String command; |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| command = "cmd /c \"tasklist /FI \"PID eq " + pid + "\" | findstr " + pid |
| + "\""; |
| } else { |
| command = "kill -0 " + pid; |
| } |
| try { |
| Process p = Runtime.getRuntime().exec(command); |
| // If the process ends with non-0 it means the process doesn't exist |
| return p.waitFor() == 0; |
| } catch (IOException e) { |
| logger.warning("Got IOException while checking running processes:\n%s", |
| Arrays.toString(e.getStackTrace())); |
| // To be safe return true |
| return true; |
| } catch (InterruptedException e) { |
| logger.warning("Got InterruptedException while checking running processes:\n%s", |
| Arrays.toString(e.getStackTrace())); |
| // To be safe return true |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Log info about a running AVD. |
| // This is intended to help identify why we occasionally get a false report |
| // that an AVD instance is already executing. |
| @Slow |
| public void logRunningAvdInfo(@NonNull AvdInfo info, @NonNull ILogger logger) { |
| String pid; |
| try { |
| pid = getAvdPid(info); |
| } |
| catch (IOException ex) { |
| logger.error(ex, "AVD not launched but got IOException while getting PID"); |
| return; |
| } |
| if (pid == null) { |
| logger.warning("AVD not launched but PID is null. Should not have indicated that the AVD is running."); |
| return; |
| } |
| logger.warning("AVD not launched because an instance appears to be running on PID " + pid); |
| String command; |
| int numTermChars; |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| command = "cmd /c \"tasklist /FI \"PID eq " + pid + "\" /FO csv /V /NH\""; |
| numTermChars = 2; // <CR><LF> |
| } |
| else { |
| command = "ps -o pid= -o user= -o pcpu= -o tty= -o stat= -o time= -o etime= -o cmd= -p " + pid; |
| numTermChars = 1; // <LF> |
| } |
| try { |
| Process proc = Runtime.getRuntime().exec(command); |
| if (proc.waitFor() != 0) { |
| logger.warning("Could not get info for that AVD process"); |
| } |
| else { |
| InputStream procInfoStream = proc.getInputStream(); // proc's output is our input |
| final int strMax = 256; |
| byte[] procInfo = new byte[strMax]; |
| int nRead = procInfoStream.read(procInfo, 0, strMax); |
| if (nRead <= numTermChars) { |
| logger.warning("Info for that AVD process is null"); |
| } |
| else { |
| logger.warning("AVD process info: [" + new String(procInfo, 0, nRead - numTermChars) + "]"); |
| } |
| } |
| } |
| catch (IOException | InterruptedException ex) { |
| logger.warning("Got exception when getting info on that AVD process:\n%s", |
| Arrays.toString(ex.getStackTrace())); |
| } |
| } |
| |
| @Slow |
| public void stopAvd(@NonNull AvdInfo info) { |
| try { |
| String pid = getAvdPid(info); |
| if (pid != null) { |
| String command; |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| command = "cmd /c \"taskkill /PID " + pid + "\""; |
| } else { |
| command = "kill " + pid; |
| } |
| try { |
| Process p = Runtime.getRuntime().exec(command); |
| // If the process ends with non-0 it means the process doesn't exist |
| p.waitFor(); |
| } catch (InterruptedException e) { |
| } |
| } |
| } |
| catch (IOException e) { |
| } |
| } |
| |
| private String getAvdPid(@NonNull AvdInfo info) throws IOException { |
| // this is a file on Unix, and a directory on Windows. |
| File f = new File(info.getDataFolderPath(), "hardware-qemu.ini.lock"); //$NON-NLS-1$ |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| f = new File(f, "pid"); |
| } |
| // This is an alternative identifier for Unix and Windows when the above one is missing. |
| File alternative = new File(info.getDataFolderPath(), "userdata-qemu.img.lock"); |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| alternative = new File(alternative, "pid"); |
| } |
| if (mFop.exists(f)) { |
| return mFop.readText(f).trim(); |
| } |
| if (mFop.exists(alternative)) { |
| return mFop.readText(alternative).trim(); |
| } |
| return null; |
| } |
| |
| /** |
| * Reloads the AVD list. |
| * |
| * @param log the log object to receive action logs. Cannot be null. |
| * @throws AndroidLocationsException if there was an error finding the location of the AVD |
| * folder. |
| */ |
| @Slow |
| public void reloadAvds(@NonNull ILogger log) throws AndroidLocationsException { |
| // 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<>(); |
| buildAvdList(allList, log); |
| |
| synchronized (mAllAvdList) { |
| mAllAvdList.clear(); |
| mAllAvdList.addAll(allList); |
| mValidAvdList = mBrokenAvdList = null; |
| } |
| } |
| |
| /** |
| * Reloads a single AVD but does not update the list. |
| * |
| * @param avdInfo an existing AVD |
| * @param log the log object to receive action logs |
| * @return an updated AVD |
| */ |
| @Slow |
| public AvdInfo reloadAvd(@NonNull AvdInfo avdInfo, @NonNull ILogger log) { |
| AvdInfo newInfo = parseAvdInfo(avdInfo.getIniFile(), log); |
| synchronized (mAllAvdList) { |
| int index = mAllAvdList.indexOf(avdInfo); |
| if (index >= 0) { |
| // Update the existing list of AVDs, unless the original AVD is not found, in which |
| // case someone else may already have updated the list. |
| replaceAvd(avdInfo, newInfo); |
| } |
| } |
| return newInfo; |
| } |
| |
| /** |
| * 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. Unless you want |
| * to locate it in a specific directory, the ideal default is {@code |
| * AvdManager.AvdInfo.getAvdFolder}. |
| * @param avdName the name of the AVD |
| * @param systemImage the system image of the AVD |
| * @param skinFolder the skin folder path to use, if specified. Can be null. |
| * @param skinName the name of the skin. Can be null. Must have been verified by caller. Can be |
| * a size in the form "NNNxMMM" or a directory name matching skinFolder. |
| * @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 bootProps the optional boot properties for the AVD. Can be null. |
| * @param removePrevious If true remove any previous files. |
| * @param editExisting If true, edit an existing AVD, changing only the minimum required. This |
| * won't remove files unless required or unless {@code removePrevious} is set. |
| * @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. |
| */ |
| @Nullable |
| @Slow |
| public AvdInfo createAvd( |
| @NonNull Path avdFolder, |
| @NonNull String avdName, |
| @NonNull ISystemImage systemImage, |
| @Nullable Path skinFolder, |
| @Nullable String skinName, |
| @Nullable String sdcard, |
| @Nullable Map<String, String> hardwareConfig, |
| @Nullable Map<String, String> bootProps, |
| boolean deviceHasPlayStore, |
| boolean removePrevious, |
| boolean editExisting, |
| @NonNull ILogger log) { |
| if (log == null) { |
| throw new IllegalArgumentException("log cannot be null"); |
| } |
| |
| File iniFile = null; |
| boolean needCleanup = false; |
| try { |
| AvdInfo newAvdInfo = null; |
| HashMap<String, String> configValues = new HashMap<>(); |
| if (!CancellableFileIo.exists(avdFolder)) { |
| // create the AVD folder. |
| Files.createDirectories(avdFolder); |
| inhibitCopyOnWrite(mFop.toFile(avdFolder), log); |
| // We're not editing an existing AVD. |
| editExisting = false; |
| } |
| else if (removePrevious) { |
| // AVD already exists and removePrevious is set, try to remove the |
| // directory's content first (but not the directory itself). |
| try { |
| deleteContentOf(mFop.toFile(avdFolder)); |
| inhibitCopyOnWrite(mFop.toFile(avdFolder), log); |
| } |
| catch (SecurityException e) { |
| log.warning("Failed to delete %1$s: %2$s", avdFolder.toAbsolutePath(), e); |
| } |
| } |
| else if (!editExisting) { |
| // The AVD already exists, we want to keep it, and we're not |
| // editing it. We must be making a copy. Duplicate the folder. |
| String oldAvdFolderPath = avdFolder.toAbsolutePath().toString(); |
| newAvdInfo = duplicateAvd(mFop.toFile(avdFolder), avdName, systemImage, log); |
| if (newAvdInfo == null) { |
| return null; |
| } |
| avdFolder = mFop.toPath(newAvdInfo.getDataFolderPath()); |
| configValues.putAll(newAvdInfo.getProperties()); |
| // If the hardware config includes an SD Card path in the old directory, |
| // update the path to the new directory |
| if (hardwareConfig != null) { |
| String oldSdCardPath = hardwareConfig.get(AVD_INI_SDCARD_PATH); |
| if (oldSdCardPath != null && oldSdCardPath.startsWith(oldAvdFolderPath)) { |
| // The hardware config points to the old directory. Substitute the new directory. |
| hardwareConfig.put(AVD_INI_SDCARD_PATH, oldSdCardPath.replace(oldAvdFolderPath, newAvdInfo.getDataFolderPath())); |
| } |
| } |
| } |
| |
| // Write the AVD ini file |
| iniFile = |
| createAvdIniFile( |
| avdName, |
| mFop.toFile(avdFolder), |
| removePrevious, |
| systemImage.getAndroidVersion()); |
| |
| needCleanup = true; |
| |
| createAvdUserdata(systemImage, avdFolder, log); |
| createAvdConfigFile(systemImage, configValues, log); |
| |
| // Tag and abi type |
| IdDisplay tag = systemImage.getTag(); |
| configValues.put(AVD_INI_TAG_ID, tag.getId()); |
| configValues.put(AVD_INI_TAG_DISPLAY, tag.getDisplay()); |
| configValues.put(AVD_INI_ABI_TYPE, systemImage.getAbiType()); |
| configValues.put(AVD_INI_PLAYSTORE_ENABLED, Boolean.toString(deviceHasPlayStore && systemImage.hasPlayStore())); |
| configValues.put(AVD_INI_ARC, Boolean.toString(SystemImage.CHROMEOS_TAG.equals(tag))); |
| |
| createAvdSkin( |
| skinFolder == null ? null : mFop.toFile(skinFolder), |
| skinName, |
| configValues, |
| log); |
| createAvdSdCard(sdcard, editExisting, configValues, mFop.toFile(avdFolder), log); |
| |
| if (hardwareConfig == null) { |
| hardwareConfig = new HashMap<>(); |
| } |
| writeCpuArch(systemImage, hardwareConfig, log); |
| |
| addHardwareConfig(systemImage, skinFolder, avdFolder, hardwareConfig, configValues, log); |
| |
| if (bootProps != null && !bootProps.isEmpty()) { |
| Path bootPropsFile = avdFolder.resolve(BOOT_PROP); |
| writeIniFile(mFop.toFile(bootPropsFile), bootProps, false); |
| } |
| |
| AvdInfo oldAvdInfo = getAvd(avdName, false /*validAvdOnly*/); |
| |
| if (newAvdInfo == null) { |
| newAvdInfo = |
| createAvdInfoObject( |
| systemImage, |
| avdName, |
| removePrevious, |
| editExisting, |
| iniFile, |
| mFop.toFile(avdFolder), |
| oldAvdInfo, |
| configValues); |
| } |
| |
| if ((removePrevious || editExisting) && |
| oldAvdInfo != null && |
| !oldAvdInfo.getDataFolderPath().equals(newAvdInfo.getDataFolderPath())) { |
| log.warning("Removing previous AVD directory at %s", |
| oldAvdInfo.getDataFolderPath()); |
| // Remove the old data directory |
| File dir = new File(oldAvdInfo.getDataFolderPath()); |
| try { |
| deleteContentOf(dir); |
| mFop.delete(dir); |
| } catch (SecurityException e) { |
| log.warning("Failed to delete %1$s: %2$s", dir.getAbsolutePath(), e); |
| } |
| } |
| |
| needCleanup = false; |
| return newAvdInfo; |
| } catch (AvdMgrException e) { |
| // Warning has already been logged |
| } catch (SecurityException | AndroidLocationsException | IOException e) { |
| log.warning("%1$s", e); |
| } finally { |
| if (needCleanup) { |
| if (iniFile != null && mFop.exists(iniFile)) { |
| mFop.delete(iniFile); |
| } |
| |
| try { |
| FileOpUtils.deleteFileOrFolder(avdFolder); |
| } catch (SecurityException e) { |
| log.warning("Failed to delete %1$s: %2$s", avdFolder.toAbsolutePath(), e); |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Duplicates an existing AVD. |
| * Update the 'config.ini' and 'hw-qemu.ini' files |
| * to reference the new name and path. |
| * |
| * @param origAvd the AVD to be duplicated |
| * @param newAvdName name of the new copy |
| * @param systemImage system image that the AVD uses |
| * @param log error logger |
| */ |
| @Nullable |
| private AvdInfo duplicateAvd( |
| @NonNull File origAvd, |
| @NonNull String newAvdName, |
| @NonNull ISystemImage systemImage, |
| @NonNull ILogger log) { |
| |
| try { |
| File destAvdFolder = new File(origAvd.getParent(), newAvdName + AVD_FOLDER_EXTENSION); |
| inhibitCopyOnWrite(destAvdFolder, log); |
| |
| ProgressIndicator progInd = new ConsoleProgressIndicator(); |
| progInd.setText("Copying files"); |
| progInd.setIndeterminate(true); |
| FileOpUtils.recursiveCopy(origAvd, destAvdFolder, mFop, progInd); |
| |
| // Modify the ID and display name in the new config.ini |
| File configIni = new File(destAvdFolder, CONFIG_INI); |
| Map<String, String> configVals = |
| parseIniFile(new PathFileWrapper(mFop.toPath(configIni)), log); |
| configVals.put(AVD_INI_AVD_ID, newAvdName); |
| configVals.put(AVD_INI_DISPLAY_NAME, newAvdName); |
| writeIniFile(configIni, configVals, true); |
| |
| // Update the AVD name and paths in the new copies of config.ini and hardware-qemu.ini |
| String origAvdName = origAvd.getName().replace(".avd", ""); |
| String origAvdPath = origAvd.getAbsolutePath(); |
| String newAvdPath = destAvdFolder.getAbsolutePath(); |
| |
| configVals = updateNameAndIniPaths(configIni, origAvdName, origAvdPath, newAvdName, newAvdPath, log); |
| |
| File hwQemu = new File(destAvdFolder, HARDWARE_QEMU_INI); |
| updateNameAndIniPaths(hwQemu, origAvdName, origAvdPath, newAvdName, newAvdPath, log); |
| |
| // Create <AVD name>.ini |
| File iniFile = createAvdIniFile(newAvdName, destAvdFolder, false, |
| systemImage.getAndroidVersion()); |
| |
| // Create an AVD object from these files |
| return new AvdInfo( |
| newAvdName, |
| iniFile, |
| destAvdFolder.getAbsolutePath(), |
| systemImage, |
| configVals); |
| } catch (AndroidLocationsException | IOException e) { |
| log.warning("Exception while duplicating an AVD: %1$s", e); |
| return null; |
| } |
| } |
| |
| /** |
| * Modifies an ini file to switch values from an old AVD name and path to a new AVD name and |
| * path. Values that are {@code oldName} are switched to {@code newName} Values that start with |
| * {@code oldPath} are modified to start with {@code newPath} |
| * |
| * @return the updated ini settings |
| */ |
| @Nullable |
| private Map<String, String> updateNameAndIniPaths( |
| @NonNull File iniFile, |
| @NonNull String oldName, |
| @NonNull String oldPath, |
| @NonNull String newName, |
| @NonNull String newPath, |
| @NonNull ILogger log) |
| throws IOException { |
| Map<String, String> iniVals = parseIniFile(new PathFileWrapper(mFop.toPath(iniFile)), log); |
| if (iniVals != null) { |
| for (Map.Entry<String, String> iniEntry : iniVals.entrySet()) { |
| String origIniValue = iniEntry.getValue(); |
| if (origIniValue.equals(oldName)) { |
| iniVals.put(iniEntry.getKey(), newName); |
| } |
| if (origIniValue.startsWith(oldPath)) { |
| String newIniValue = origIniValue.replace(oldPath, newPath); |
| iniVals.put(iniEntry.getKey(), newIniValue); |
| } |
| } |
| writeIniFile(iniFile, iniVals, true); |
| } |
| return iniVals; |
| } |
| |
| /** |
| * 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(@NonNull ISystemImage systemImage) |
| throws InvalidTargetPathException { |
| |
| Path folder = systemImage.getLocation(); |
| String imageFullPath = folder.toAbsolutePath().toString(); |
| |
| // make this path relative to the SDK location |
| String sdkLocation = mSdkHandler.getLocation().toAbsolutePath().toString(); |
| if (!imageFullPath.startsWith(sdkLocation)) { |
| // this really really should not happen. |
| assert false; |
| throw new InvalidTargetPathException("Target location is not inside the SDK."); |
| } |
| |
| String[] list; |
| try (Stream<Path> contents = CancellableFileIo.list(folder)) { |
| list = |
| contents.map(path -> path.getFileName().toString()) |
| .filter(path -> IMAGE_NAME_PATTERN.matcher(path).matches()) |
| .toArray(String[]::new); |
| } catch (IOException e) { |
| return null; |
| } |
| if (list.length > 0) { |
| // Remove the SDK root path, e.g. /sdk/dir1/dir2 -> /dir1/dir2 |
| imageFullPath = imageFullPath.substring(sdkLocation.length()); |
| // The path is relative, so it must not start with a file separator |
| if (imageFullPath.charAt(0) == File.separatorChar) { |
| imageFullPath = imageFullPath.substring(1); |
| } |
| // For compatibility with previous versions, we denote folders |
| // by ending the path with file separator |
| if (!imageFullPath.endsWith(File.separator)) { |
| imageFullPath += File.separator; |
| } |
| |
| return imageFullPath; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Creates the ini file for an AVD. |
| * |
| * @param name of the AVD. |
| * @param avdFolder path for the data folder of the AVD. |
| * @param removePrevious True if an existing ini file should be removed. |
| * @throws AndroidLocationsException if there's a problem getting android root directory. |
| * @throws IOException if {@link File#getAbsolutePath()} fails. |
| */ |
| private File createAvdIniFile( |
| @NonNull String name, |
| @NonNull File avdFolder, |
| boolean removePrevious, |
| @NonNull AndroidVersion version) |
| throws AndroidLocationsException, IOException { |
| File iniFile = AvdInfo.getDefaultIniFile(this, name); |
| |
| if (removePrevious) { |
| if (mFop.isFile(iniFile)) { |
| mFop.delete(iniFile); |
| } else if (mFop.isDirectory(iniFile)) { |
| deleteContentOf(iniFile); |
| mFop.delete(iniFile); |
| } |
| } |
| |
| String absPath = avdFolder.getAbsolutePath(); |
| String relPath = null; |
| Path androidFolder = mSdkHandler.getAndroidFolder(); |
| if (androidFolder == null) { |
| throw new AndroidLocationsException( |
| "Can't locate Android SDK installation directory for the AVD .ini file."); |
| } |
| String androidPath = androidFolder.toAbsolutePath().toString() + File.separator; |
| if (absPath.startsWith(androidPath)) { |
| // Compute the AVD path relative to the android path. |
| assert androidPath.endsWith(File.separator); |
| relPath = absPath.substring(androidPath.length()); |
| } |
| |
| HashMap<String, String> values = new HashMap<>(); |
| if (relPath != null) { |
| values.put(AVD_INFO_REL_PATH, relPath); |
| } |
| values.put(AVD_INFO_ABS_PATH, absPath); |
| values.put(AVD_INFO_TARGET, AndroidTargetHash.getPlatformHashString(version)); |
| writeIniFile(iniFile, values, true); |
| |
| return iniFile; |
| } |
| |
| /** |
| * Creates the ini file for an AVD. |
| * |
| * @param info of the AVD. |
| * @throws AndroidLocationsException if there's a problem getting android root directory. |
| * @throws IOException if {@link File#getAbsolutePath()} fails. |
| */ |
| private File createAvdIniFile(@NonNull AvdInfo info) |
| throws AndroidLocationsException, IOException { |
| return createAvdIniFile(info.getName(), |
| new File(info.getDataFolderPath()), |
| false, info.getAndroidVersion()); |
| } |
| |
| /** |
| * 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. |
| */ |
| @Slow |
| public boolean deleteAvd(@NonNull AvdInfo avdInfo, @NonNull ILogger log) { |
| try { |
| boolean error = false; |
| |
| File f = avdInfo.getIniFile(); |
| if (f != null && mFop.exists(f)) { |
| log.info("Deleting file %1$s\n", f.getCanonicalPath()); |
| if (!mFop.delete(f)) { |
| log.warning("Failed to delete %1$s\n", f.getCanonicalPath()); |
| error = true; |
| } |
| } |
| |
| String path = avdInfo.getDataFolderPath(); |
| if (path != null) { |
| f = new File(path); |
| if (mFop.exists(f)) { |
| log.info("Deleting folder %1$s\n", f.getCanonicalPath()); |
| if (deleteContentOf(f) == false || mFop.delete(f) == false) { |
| log.warning("Failed to delete %1$s\n", f.getCanonicalPath()); |
| error = true; |
| } |
| } |
| } |
| |
| removeAvd(avdInfo); |
| |
| if (error) { |
| log.info("\nAVD '%1$s' deleted with errors. See errors above.\n", |
| avdInfo.getName()); |
| } else { |
| log.info("\nAVD '%1$s' deleted.\n", avdInfo.getName()); |
| return true; |
| } |
| |
| } catch (IOException | SecurityException e) { |
| log.warning("%1$s", e); |
| } |
| 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. |
| */ |
| @Slow |
| public boolean moveAvd( |
| @NonNull AvdInfo avdInfo, |
| @Nullable String newName, |
| @Nullable String paramFolderPath, |
| @NonNull ILogger log) { |
| try { |
| if (paramFolderPath != null) { |
| File f = new File(avdInfo.getDataFolderPath()); |
| log.info("Moving '%1$s' to '%2$s'.\n", avdInfo.getDataFolderPath(), paramFolderPath); |
| if (!mFop.renameTo(f, new File(paramFolderPath))) { |
| log.error(null, "Failed to move '%1$s' to '%2$s'.\n", |
| avdInfo.getDataFolderPath(), paramFolderPath); |
| return false; |
| } |
| |
| // update AVD info |
| AvdInfo info = new AvdInfo( |
| avdInfo.getName(), |
| avdInfo.getIniFile(), |
| paramFolderPath, |
| avdInfo.getSystemImage(), |
| avdInfo.getProperties()); |
| replaceAvd(avdInfo, info); |
| |
| // update the ini file |
| createAvdIniFile(info); |
| } |
| |
| if (newName != null) { |
| File oldIniFile = avdInfo.getIniFile(); |
| File newIniFile = AvdInfo.getDefaultIniFile(this, newName); |
| |
| log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath()); |
| if (!mFop.renameTo(oldIniFile, newIniFile)) { |
| log.warning(null, "Failed to move '%1$s' to '%2$s'.", |
| oldIniFile.getPath(), newIniFile.getPath()); |
| return false; |
| } |
| |
| // update AVD info |
| AvdInfo info = new AvdInfo( |
| newName, |
| avdInfo.getIniFile(), |
| avdInfo.getDataFolderPath(), |
| avdInfo.getSystemImage(), |
| avdInfo.getProperties()); |
| replaceAvd(avdInfo, info); |
| } |
| |
| log.info("AVD '%1$s' moved.\n", avdInfo.getName()); |
| |
| } catch (AndroidLocationsException | IOException e) { |
| log.warning("$1%s", e); |
| return false; |
| } |
| |
| // 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 = mFop.listFiles(folder); |
| if (files != null) { |
| for (File f : files) { |
| if (mFop.isDirectory(f)) { |
| if (deleteContentOf(f) == false) { |
| return false; |
| } |
| } |
| if (mFop.delete(f) == 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. |
| * |
| * <p>Note: the method is to be considered private. It is made protected so that unit tests can |
| * easily override the AVD root. |
| * |
| * @return A new {@link File} array or null. The array might be empty. |
| * @throws AndroidLocationsException if there's a problem getting android root directory. |
| */ |
| private File[] buildAvdFilesList() throws AndroidLocationsException { |
| // ensure folder validity. |
| if (mFop.isFile(mBaseAvdFolder)) { |
| throw new AndroidLocationsException( |
| String.format("%1$s is not a valid folder.", mBaseAvdFolder.getAbsolutePath())); |
| } else if (mFop.exists(mBaseAvdFolder) == false) { |
| // folder is not there, we create it and return |
| mFop.mkdirs(mBaseAvdFolder); |
| return null; |
| } |
| |
| File[] avds = mFop.listFiles(mBaseAvdFolder, (parent, name) -> { |
| if (INI_NAME_PATTERN.matcher(name).matches()) { |
| // check it's a file and not a folder |
| return mFop.isFile(new File(parent, name)); |
| } |
| |
| 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 AndroidLocationsException if there's a problem getting android root directory. |
| */ |
| private void buildAvdList(ArrayList<AvdInfo> allList, ILogger log) |
| throws AndroidLocationsException { |
| File[] avds = buildAvdFilesList(); |
| if (avds != null) { |
| for (File avd : avds) { |
| AvdInfo info = parseAvdInfo(avd, log); |
| if (info != null && !allList.contains(info)) { |
| allList.add(info); |
| } |
| } |
| } |
| } |
| |
| private DeviceManager getDeviceManager(ILogger logger) { |
| DeviceManager manager = mDeviceManagers.get(logger); |
| if (manager == null) { |
| manager = DeviceManager.createInstance(mSdkHandler, logger); |
| manager.registerListener(mDeviceManagers::clear); |
| mDeviceManagers.put(logger, manager); |
| } |
| return manager; |
| } |
| |
| /** |
| * Parses an AVD .ini file to create an {@link AvdInfo}. |
| * |
| * @param iniPath 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. |
| */ |
| @VisibleForTesting |
| @Slow |
| public AvdInfo parseAvdInfo(@NonNull File iniPath, @NonNull ILogger log) { |
| Map<String, String> map = parseIniFile(new PathFileWrapper(mFop.toPath(iniPath)), log); |
| |
| String avdPath = null; |
| if (map != null) { |
| avdPath = map.get(AVD_INFO_ABS_PATH); |
| if (avdPath == null || !(mFop.isDirectory(new File(avdPath)))) { |
| // Try to fallback on the relative path, if present. |
| String relPath = map.get(AVD_INFO_REL_PATH); |
| if (relPath != null) { |
| Path androidFolder = mSdkHandler.getAndroidFolder(); |
| Path f = |
| androidFolder == null |
| ? mSdkHandler.getFileOp().toPath(relPath) |
| : androidFolder.resolve(relPath); |
| if (CancellableFileIo.isDirectory(f)) { |
| avdPath = f.toAbsolutePath().toString(); |
| } |
| } |
| } |
| } |
| if (avdPath == null || !(mFop.isDirectory(new File(avdPath)))) { |
| // Corrupted .ini file |
| String avdName = iniPath.getName(); |
| if (avdName.endsWith(".ini")) { |
| avdName = avdName.substring(0, avdName.length() - 4); |
| } |
| return new AvdInfo(avdName, iniPath, iniPath.getPath(), null, null, AvdStatus.ERROR_CORRUPTED_INI); |
| } |
| |
| PathFileWrapper configIniFile = null; |
| Map<String, String> properties = null; |
| LoggerProgressIndicatorWrapper progress = |
| new LoggerProgressIndicatorWrapper(log) { |
| @Override |
| public void logVerbose(@NonNull String s) { |
| // Skip verbose messages } |
| } |
| }; |
| |
| // load the AVD properties. |
| if (avdPath != null) { |
| configIniFile = new PathFileWrapper(mFop.toPath(avdPath).resolve(CONFIG_INI)); |
| } |
| |
| if (configIniFile != null) { |
| if (!configIniFile.exists()) { |
| log.warning("Missing file '%1$s'.", configIniFile.getOsLocation()); |
| } else { |
| properties = parseIniFile(configIniFile, log); |
| } |
| } |
| |
| // get name |
| String name = iniPath.getName(); |
| Matcher matcher = INI_NAME_PATTERN.matcher(iniPath.getName()); |
| if (matcher.matches()) { |
| name = matcher.group(1); |
| } |
| |
| // Check if the value of image.sysdir.1 is valid. |
| boolean validImageSysdir = true; |
| String imageSysDir = null; |
| ISystemImage sysImage = null; |
| if (properties != null) { |
| imageSysDir = properties.get(AVD_INI_IMAGES_1); |
| if (imageSysDir != null) { |
| Path sdkLocation = mSdkHandler.getLocation(); |
| Path imageDir = |
| sdkLocation == null |
| ? mFop.toPath(imageSysDir) |
| : sdkLocation.resolve(imageSysDir); |
| sysImage = mSdkHandler.getSystemImageManager(progress).getImageAt(imageDir); |
| } |
| } |
| |
| |
| // Get the device status if this AVD is associated with a device |
| DeviceStatus deviceStatus = null; |
| boolean updateHashV2 = false; |
| if (properties != null) { |
| String deviceName = properties.get(AVD_INI_DEVICE_NAME); |
| String deviceMfctr = properties.get(AVD_INI_DEVICE_MANUFACTURER); |
| |
| Device d; |
| |
| if (deviceName != null && deviceMfctr != null) { |
| DeviceManager devMan = getDeviceManager(log); |
| d = devMan.getDevice(deviceName, deviceMfctr); |
| deviceStatus = d == null ? DeviceStatus.MISSING : DeviceStatus.EXISTS; |
| |
| if (d != null) { |
| updateHashV2 = true; |
| String hashV2 = properties.get(AVD_INI_DEVICE_HASH_V2); |
| if (hashV2 != null) { |
| String newHashV2 = DeviceManager.hasHardwarePropHashChanged(d, hashV2); |
| if (newHashV2 == null) { |
| updateHashV2 = false; |
| } else { |
| properties.put(AVD_INI_DEVICE_HASH_V2, newHashV2); |
| } |
| } |
| |
| String hashV1 = properties.get(AVD_INI_DEVICE_HASH_V1); |
| if (hashV1 != null) { |
| // will recompute a hash v2 and save it below |
| properties.remove(AVD_INI_DEVICE_HASH_V1); |
| } |
| } |
| } |
| } |
| |
| |
| // 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 (properties == null || imageSysDir == null) { |
| status = AvdStatus.ERROR_PROPERTIES; |
| } else if (!validImageSysdir) { |
| status = AvdStatus.ERROR_IMAGE_DIR; |
| } else if (deviceStatus == DeviceStatus.CHANGED) { |
| status = AvdStatus.ERROR_DEVICE_CHANGED; |
| } else if (deviceStatus == DeviceStatus.MISSING) { |
| status = AvdStatus.ERROR_DEVICE_MISSING; |
| } else if (sysImage == null) { |
| status = AvdStatus.ERROR_IMAGE_MISSING; |
| } else { |
| status = AvdStatus.OK; |
| } |
| |
| if (properties == null) { |
| properties = Maps.newHashMap(); |
| } |
| |
| if (!properties.containsKey(AVD_INI_ANDROID_API) && |
| !properties.containsKey(AVD_INI_ANDROID_CODENAME)) { |
| String targetHash = map.get(AVD_INFO_TARGET); |
| AndroidVersion version = AndroidTargetHash.getVersionFromHash(targetHash); |
| if (version != null) { |
| properties.put(AVD_INI_ANDROID_API, Integer.toString(version.getApiLevel())); |
| if (version.getCodename() != null) { |
| properties.put(AVD_INI_ANDROID_CODENAME, version.getCodename()); |
| } |
| } |
| } |
| |
| AvdInfo info = new AvdInfo( |
| name, |
| iniPath, |
| avdPath, |
| sysImage, |
| properties, |
| status); |
| |
| if (updateHashV2) { |
| try { |
| return updateDeviceChanged(info, log); |
| } catch (IOException ignore) {} |
| } |
| |
| return info; |
| } |
| |
| /** |
| * Writes a .ini file from a set of properties, using UTF-8 encoding. |
| * The keys are sorted. |
| * The file should be read back later by {@link #parseIniFile(IAbstractFile, ILogger)}. |
| * |
| * @param iniFile The file to generate. |
| * @param values The properties to place in the ini file. |
| * @param addEncoding When true, add a property {@link #AVD_INI_ENCODING} indicating the |
| * encoding used to write the file. |
| * @throws IOException if {@link FileWriter} fails to open, write or close the file. |
| */ |
| private void writeIniFile(File iniFile, Map<String, String> values, boolean addEncoding) |
| throws IOException { |
| |
| Charset charset = Charsets.UTF_8; |
| try (OutputStreamWriter writer = new OutputStreamWriter(mFop.newFileOutputStream(iniFile), |
| charset)){ |
| if (addEncoding) { |
| // Write down the charset we're using in case we want to use it later. |
| values.put(AVD_INI_ENCODING, charset.name()); |
| } |
| |
| ArrayList<String> keys = new ArrayList<>(values.keySet()); |
| // Do not save these values (always recompute) |
| keys.remove(AVD_INI_ANDROID_API); |
| keys.remove(AVD_INI_ANDROID_CODENAME); |
| Collections.sort(keys); |
| |
| for (String key : keys) { |
| String value = values.get(key); |
| if (value != null) { |
| writer.write(String.format("%1$s=%2$s\n", key, value)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Parses a property file and returns a map of the content. |
| * |
| * <p>If the file is not present, null is returned with no error messages sent to the log. |
| * |
| * <p>Charset encoding will be either the system's default or the one specified by the {@link |
| * #AVD_INI_ENCODING} key if present. |
| * |
| * @param propFile the property file to parse |
| * @param log the ILogger object receiving warning/error from the parsing. |
| * @return the map of (key,value) pairs, or null if the parsing failed. |
| */ |
| @Slow |
| public static Map<String, String> parseIniFile( |
| @NonNull IAbstractFile propFile, @Nullable ILogger log) { |
| return parseIniFileImpl(propFile, log, null); |
| } |
| |
| /** |
| * Implementation helper for the {@link #parseIniFile(IAbstractFile, ILogger)} method. |
| * Don't call this one directly. |
| * |
| * @param propFile the property file to parse |
| * @param log the ILogger object receiving warning/error from the parsing. |
| * @param charset When a specific charset is specified, this will be used as-is. |
| * When null, the default charset will first be used and if the key |
| * {@link #AVD_INI_ENCODING} is found the parsing will restart using that specific |
| * charset. |
| * @return the map of (key,value) pairs, or null if the parsing failed. |
| */ |
| private static Map<String, String> parseIniFileImpl( |
| @NonNull IAbstractFile propFile, |
| @Nullable ILogger log, |
| @Nullable Charset charset) { |
| |
| BufferedReader reader = null; |
| try { |
| boolean canChangeCharset = false; |
| if (charset == null) { |
| canChangeCharset = true; |
| charset = Charsets.ISO_8859_1; |
| } |
| reader = new BufferedReader(new InputStreamReader(propFile.getContents(), charset)); |
| |
| String line = null; |
| Map<String, String> map = new HashMap<>(); |
| while ((line = reader.readLine()) != null) { |
| line = line.trim(); |
| if (!line.isEmpty() && line.charAt(0) != '#') { |
| |
| Matcher m = INI_LINE_PATTERN.matcher(line); |
| if (m.matches()) { |
| // Note: we do NOT escape values. |
| String key = m.group(1); |
| String value = m.group(2); |
| |
| // If we find the charset encoding and it's not the same one and |
| // it's a valid one, re-read the file using that charset. |
| if (canChangeCharset && |
| AVD_INI_ENCODING.equals(key) && |
| !charset.name().equals(value) && |
| Charset.isSupported(value)) { |
| charset = Charset.forName(value); |
| return parseIniFileImpl(propFile, log, charset); |
| } |
| |
| map.put(key, value); |
| } else { |
| if (log != null) { |
| log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", |
| propFile.getOsLocation(), |
| line); |
| } |
| return null; |
| } |
| } |
| } |
| |
| return map; |
| } catch (FileNotFoundException e) { |
| // this should not happen since we usually test the file existence before |
| // calling the method. |
| // Return null below. |
| } catch (IOException | StreamException e) { |
| if (log != null) { |
| log.warning("Error parsing '%1$s': %2$s.", |
| propFile.getOsLocation(), |
| e.getMessage()); |
| } |
| } finally { |
| try { |
| Closeables.close(reader, true /* swallowIOException */); |
| } catch (IOException e) { |
| // cannot happen. |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * 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. |
| */ |
| @VisibleForTesting |
| protected boolean createSdCard(String toolLocation, String size, String location, ILogger log) { |
| try { |
| String[] command = new String[3]; |
| command[0] = toolLocation; |
| command[1] = size; |
| command[2] = location; |
| Process process = Runtime.getRuntime().exec(command); |
| |
| final ArrayList<String> errorOutput = new ArrayList<>(); |
| final ArrayList<String> stdOutput = new ArrayList<>(); |
| |
| int status = GrabProcessOutput.grabProcessOutput( |
| process, |
| Wait.WAIT_FOR_READERS, |
| new IProcessOutput() { |
| @Override |
| public void out(@Nullable String line) { |
| if (line != null) { |
| stdOutput.add(line); |
| } |
| } |
| |
| @Override |
| public void err(@Nullable String line) { |
| if (line != null) { |
| errorOutput.add(line); |
| } |
| } |
| }); |
| |
| if (status == 0) { |
| return true; |
| } else { |
| for (String error : errorOutput) { |
| log.warning("%1$s", error); |
| } |
| } |
| |
| } catch (InterruptedException | IOException e) { |
| // pass, print error below |
| } |
| |
| log.warning("Failed to create the SD card."); |
| return false; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| @Slow |
| public AvdInfo updateAvd(AvdInfo avd, Map<String, String> newProperties) throws IOException { |
| // now write the config file |
| File configIniFile = new File(avd.getDataFolderPath(), CONFIG_INI); |
| writeIniFile(configIniFile, newProperties, true); |
| |
| // 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.getIniFile(), |
| avd.getDataFolderPath(), |
| avd.getSystemImage(), |
| newProperties); |
| |
| replaceAvd(avd, newAvd); |
| |
| return newAvd; |
| } |
| |
| /** |
| * Updates the device-specific part of an AVD ini. |
| * |
| * @param avd the AVD to update. |
| * @param log the log object to receive action logs. Cannot be null. |
| * @return The new AVD on success. |
| */ |
| @Slow |
| public AvdInfo updateDeviceChanged(AvdInfo avd, ILogger log) throws IOException { |
| // Overwrite the properties derived from the device and nothing else |
| Map<String, String> properties = new HashMap<>(avd.getProperties()); |
| |
| DeviceManager devMan = getDeviceManager(log); |
| Collection<Device> devices = devMan.getDevices(DeviceManager.ALL_DEVICES); |
| String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME); |
| String manufacturer = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); |
| |
| if (name != null && manufacturer != null) { |
| for (Device d : devices) { |
| if (d.getId().equals(name) && d.getManufacturer().equals(manufacturer)) { |
| // The device has a RAM size, but we don't want to use it. |
| // Instead, we'll keep the AVD's existing RAM size setting. |
| final Map<String, String> deviceHwProperties = DeviceManager.getHardwareProperties(d); |
| deviceHwProperties.remove(AVD_INI_RAM_SIZE); |
| properties.putAll(deviceHwProperties); |
| try { |
| return updateAvd(avd, properties); |
| } catch (IOException e) { |
| log.warning("%1$s", e); |
| } |
| } |
| } |
| } else { |
| log.warning("Base device information incomplete or missing."); |
| } |
| return null; |
| } |
| |
| /** |
| * Sets the paths to the system images in a properties map. |
| * |
| * @param image the system image for this avd. |
| * @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(ISystemImage image, |
| Map<String, String> properties, |
| ILogger 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(image); |
| if (imagePath != null) { |
| properties.put(property, imagePath); |
| return true; |
| } |
| } catch (InvalidTargetPathException e) { |
| log.warning("%1$s", e); |
| } |
| |
| 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; |
| } |
| } |
| |
| /** |
| * Create the user data file for an AVD |
| * |
| * @param systemImage the system image of the AVD |
| * @param avdFolder where the AVDs live |
| * @param log receives error messages |
| */ |
| private void createAvdUserdata( |
| @NonNull ISystemImage systemImage, @NonNull Path avdFolder, @NonNull ILogger log) |
| throws IOException, AvdMgrException { |
| // Copy userdata.img from system-images to the *.avd directory |
| Path imageFolder = systemImage.getLocation(); |
| Path userdataSrc = imageFolder.resolve(USERDATA_IMG); |
| |
| String abiType = systemImage.getAbiType(); |
| |
| if (CancellableFileIo.notExists(userdataSrc)) { |
| if (CancellableFileIo.isDirectory(imageFolder.resolve(DATA_FOLDER))) { |
| // Because this image includes a data folder, a |
| // userdata.img file is not needed. Don't signal |
| // an error. |
| // (The emulator will access the 'data' folder directly; |
| // we do not need to copy it over.) |
| return; |
| } |
| log.warning("Unable to find a '%1$s' file for ABI %2$s to copy into the AVD folder.", |
| USERDATA_IMG, |
| abiType); |
| throw new AvdMgrException(); |
| } |
| |
| Path userdataDest = avdFolder.resolve(USERDATA_IMG); |
| |
| if (CancellableFileIo.notExists(userdataDest)) { |
| Files.copy(userdataSrc, userdataDest); |
| |
| if (CancellableFileIo.notExists(userdataDest)) { |
| log.warning("Unable to create '%1$s' file in the AVD folder.", |
| userdataDest); |
| throw new AvdMgrException(); |
| } |
| } |
| } |
| |
| /** |
| * Create the configuration file for an AVD |
| * @param systemImage the system image of the AVD |
| * @param values settings for the AVD |
| * @param log receives error messages |
| */ |
| private void createAvdConfigFile( |
| @NonNull ISystemImage systemImage, |
| @Nullable HashMap<String, String> values, |
| @NonNull ILogger log) |
| throws AvdMgrException { |
| |
| if (!setImagePathProperties(systemImage, values, log)) { |
| log.warning("Failed to set image path properties in the AVD folder."); |
| throw new AvdMgrException(); |
| } |
| return; |
| } |
| |
| /** |
| * Write the CPU architecture to a new AVD |
| * @param systemImage the system image of the AVD |
| * @param values settings for the AVD |
| * @param log receives error messages |
| */ |
| private void writeCpuArch( |
| @NonNull ISystemImage systemImage, |
| @NonNull Map<String,String> values, |
| @NonNull ILogger log) |
| throws AvdMgrException { |
| |
| String abiType = systemImage.getAbiType(); |
| Abi abi = Abi.getEnum(abiType); |
| if (abi != null) { |
| String arch = abi.getCpuArch(); |
| // Chrome OS image is a special case: the system image |
| // is actually x86_64 while the android container inside |
| // it is x86. We have to set it x86_64 to let it boot |
| // under android emulator. |
| if (arch.equals(SdkConstants.CPU_ARCH_INTEL_ATOM) |
| && SystemImage.CHROMEOS_TAG.equals(systemImage.getTag())) { |
| arch = SdkConstants.CPU_ARCH_INTEL_ATOM64; |
| } |
| values.put(AVD_INI_CPU_ARCH, arch); |
| |
| String model = abi.getCpuModel(); |
| if (model != null) { |
| values.put(AVD_INI_CPU_MODEL, model); |
| } |
| } else { |
| log.warning("ABI %1$s is not supported by this version of the SDK Tools", abiType); |
| throw new AvdMgrException(); |
| } |
| } |
| |
| /** |
| * Link a skin with the new AVD |
| * @param skinFolder where the skin is |
| * @param skinName the name of the skin |
| * @param values settings for the AVD |
| * @param log receives error messages |
| */ |
| private void createAvdSkin( |
| @Nullable File skinFolder, |
| @Nullable String skinName, |
| @NonNull Map<String,String> values, |
| @NonNull ILogger log) |
| throws AvdMgrException { |
| |
| // Now the skin. |
| String skinPath = null; |
| |
| if (skinFolder == null && skinName != null && |
| NUMERIC_SKIN_SIZE.matcher(skinName).matches()) { |
| // Numeric skin size. Set both skinPath and skinName to the same size. |
| skinPath = skinName; |
| |
| } else if (skinFolder != null && skinName == null) { |
| // Skin folder is specified, but not skin name. Adjust it. |
| skinName = skinFolder.getName(); |
| } |
| |
| if (skinFolder != null) { |
| // skin does not exist! |
| if (!mFop.exists(skinFolder)) { |
| log.warning("Skin '%1$s' does not exist at %2$s.", skinName, skinFolder.getPath()); |
| throw new AvdMgrException(); |
| } |
| |
| // if skinFolder is in the sdk, use the relative path |
| if (skinFolder.getPath().startsWith(mSdkHandler.getLocation().toString())) { |
| skinPath = mSdkHandler.getLocation().relativize(mFop.toPath(skinFolder)).toString(); |
| } else { |
| // Skin isn't in the sdk. Just use the absolute path. |
| skinPath = skinFolder.getAbsolutePath(); |
| } |
| } |
| |
| // Set skin.name for display purposes in the AVD manager and |
| // set skin.path for use by the emulator. |
| if (skinName != null) { |
| values.put(AVD_INI_SKIN_NAME, skinName); |
| } |
| if (skinPath != null) { |
| values.put(AVD_INI_SKIN_PATH, skinPath); |
| } |
| } |
| |
| /** |
| * Create an SD card for the AVD |
| * @param sdcard either a size indicator or the name of a file |
| * @param editExisting true if modifying an existing AVD |
| * @param values settings for the AVD |
| * @param avdFolder where the AVDs live |
| * @param log receives error messages |
| */ |
| private void createAvdSdCard( |
| @Nullable String sdcard, |
| boolean editExisting, |
| @NonNull Map<String,String> values, |
| @NonNull File avdFolder, |
| @NonNull ILogger log) |
| throws AvdMgrException { |
| |
| if (sdcard == null || sdcard.isEmpty()) { |
| return; |
| } |
| |
| // 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. |
| |
| long sdcardSize = parseSdcardSize(sdcard, null); |
| |
| if (sdcardSize == SDCARD_SIZE_NOT_IN_RANGE) { |
| log.warning("SD Card size must be in the range 9 MiB..1023 GiB."); |
| throw new AvdMgrException(); |
| } |
| |
| if (sdcardSize == SDCARD_SIZE_INVALID) { |
| log.warning("Unable to parse SD Card size"); |
| throw new AvdMgrException(); |
| } |
| |
| if (sdcardSize == SDCARD_NOT_SIZE_PATTERN) { |
| File sdcardFile = new File(sdcard); |
| if ( !mFop.isFile(sdcardFile) ) { |
| log.warning("'%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); |
| throw new AvdMgrException(); |
| } |
| // sdcard value is an external sdcard, so we put its path into the config.ini |
| values.put(AVD_INI_SDCARD_PATH, sdcard); |
| return; |
| } |
| |
| // create the sdcard. |
| File sdcardFile = new File(avdFolder, SDCARD_IMG); |
| |
| boolean runMkSdcard = true; |
| if (mFop.exists(sdcardFile)) { |
| if (sdcardFile.length() == sdcardSize && editExisting) { |
| // There's already an sdcard file with the right size and we're |
| // not overriding it... so don't remove it. |
| runMkSdcard = false; |
| log.info("SD Card already present with same size, was not changed.\n"); |
| } |
| } |
| if (mFop.getClass().getSimpleName().equals("MockFileOp")) { |
| // We don't have a real filesystem, so we won't be able to run the tool. Skip. |
| runMkSdcard = false; |
| } |
| |
| if (runMkSdcard) { |
| String path = sdcardFile.getAbsolutePath(); |
| |
| // execute mksdcard with the proper parameters. |
| LoggerProgressIndicatorWrapper progress = new LoggerProgressIndicatorWrapper(log); |
| LocalPackage p = mSdkHandler.getLocalPackage(SdkConstants.FD_EMULATOR, progress); |
| if (p == null) { |
| progress.logWarning( |
| String.format( |
| "Unable to find %1$s in the %2$s component", |
| SdkConstants.mkSdCardCmdName(), SdkConstants.FD_EMULATOR)); |
| throw new AvdMgrException(); |
| } |
| Path mkSdCard = p.getLocation().resolve(SdkConstants.mkSdCardCmdName()); |
| |
| if (!CancellableFileIo.isRegularFile(mkSdCard)) { |
| log.warning("'%1$s' is missing from the SDK tools folder.", mkSdCard.getFileName()); |
| throw new AvdMgrException(); |
| } |
| |
| if (!createSdCard(mkSdCard.toAbsolutePath().toString(), sdcard, path, log)) { |
| // mksdcard output has already been displayed, no need to |
| // output anything else. |
| log.warning("Failed to create sdcard in the AVD folder."); |
| throw new AvdMgrException(); |
| } |
| } |
| |
| // 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); |
| } |
| |
| /** |
| * Add the hardware configuration to an AVD |
| * |
| * @param systemImage the system image of the AVD |
| * @param skinFolder where the skin is |
| * @param avdFolder where the AVDs live |
| * @param hardwareConfig map of configuration values |
| * @param values settings for the resulting AVD |
| * @param log receives error messages |
| */ |
| private void addHardwareConfig( |
| @NonNull ISystemImage systemImage, |
| @Nullable Path skinFolder, |
| @NonNull Path avdFolder, |
| @Nullable Map<String, String> hardwareConfig, |
| @Nullable Map<String, String> values, |
| @NonNull ILogger log) |
| throws IOException { |
| |
| // 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 sys img |
| // 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<>(); |
| |
| PathFileWrapper sysImgHardwareFile = |
| new PathFileWrapper(systemImage.getLocation().resolve(HARDWARE_INI)); |
| if (sysImgHardwareFile.exists()) { |
| Map<String, String> imageHardwardConfig = ProjectProperties.parsePropertyFile( |
| sysImgHardwareFile, log); |
| |
| if (imageHardwardConfig != null) { |
| finalHardwareValues.putAll(imageHardwardConfig); |
| } |
| } |
| |
| // get the hardware properties for this skin |
| if (skinFolder != null) { |
| PathFileWrapper skinHardwareFile = |
| new PathFileWrapper(skinFolder.resolve(HARDWARE_INI)); |
| if (skinHardwareFile.exists()) { |
| Map<String, String> skinHardwareConfig = |
| ProjectProperties.parsePropertyFile(skinHardwareFile, log); |
| |
| if (skinHardwareConfig != null) { |
| finalHardwareValues.putAll(skinHardwareConfig); |
| } |
| } |
| } |
| |
| // put the hardware provided by the user. |
| if (hardwareConfig != null) { |
| finalHardwareValues.putAll(hardwareConfig); |
| } |
| |
| // Finally add hardware properties |
| if (values == null) { |
| values = new HashMap<>(); |
| } |
| values.putAll(finalHardwareValues); |
| |
| Path configIniFile = avdFolder.resolve(CONFIG_INI); |
| writeIniFile(mFop.toFile(configIniFile), values, true); |
| |
| return; |
| } |
| |
| /** |
| * Create an AvdInfo object from the new AVD. |
| * @param systemImage the system image of the AVD |
| * @param avdName the name of the AVD |
| * @param removePrevious true if the existing AVD should be deleted |
| * @param editExisting true if modifying an existing AVD |
| * @param iniFile the .ini file of this AVD |
| * @param avdFolder where the AVD resides |
| * @param oldAvdInfo configuration of the old AVD |
| * @param values a map of the AVD's info |
| */ |
| @NonNull |
| private AvdInfo createAvdInfoObject( |
| @NonNull ISystemImage systemImage, |
| @NonNull String avdName, |
| boolean removePrevious, |
| boolean editExisting, |
| @NonNull File iniFile, |
| @NonNull File avdFolder, |
| @Nullable AvdInfo oldAvdInfo, |
| @Nullable Map<String,String> values) |
| throws AvdMgrException { |
| |
| // create the AvdInfo object, and add it to the list |
| AvdInfo theAvdInfo = new AvdInfo( |
| avdName, |
| iniFile, |
| avdFolder.getAbsolutePath(), |
| systemImage, |
| values); |
| |
| synchronized (mAllAvdList) { |
| if (oldAvdInfo != null && (removePrevious || editExisting)) { |
| mAllAvdList.remove(oldAvdInfo); |
| } |
| mAllAvdList.add(theAvdInfo); |
| mValidAvdList = mBrokenAvdList = null; |
| } |
| return theAvdInfo; |
| } |
| |
| |
| /** |
| * (Linux only) Sets the AVD folder to not be "Copy on Write" |
| * |
| * CoW at the file level conflicts with QEMU's explicit CoW |
| * operations and can hurt Emulator performance. |
| * NOTE: The "chatter +C" command does not impact existing |
| * files in the folder. Thus this method should be |
| * called before the folder is populated. |
| * This method is "best effort." Common failures are silently |
| * ignored. Other failures are logged and ignored. |
| * @param avdFolder where the AVD's files will be written |
| * @param log the log object to receive action logs |
| */ |
| private static void inhibitCopyOnWrite(@NonNull File avdFolder, @NonNull ILogger log) { |
| if (SdkConstants.CURRENT_PLATFORM != SdkConstants.PLATFORM_LINUX) { |
| return; |
| } |
| try { |
| String[] chattrCommand = new String[3]; |
| chattrCommand[0] = "chattr"; |
| chattrCommand[1] = "+C"; |
| chattrCommand[2] = avdFolder.getAbsolutePath(); |
| Process chattrProcess = Runtime.getRuntime().exec(chattrCommand); |
| |
| final ArrayList<String> errorOutput = new ArrayList<>(); |
| |
| GrabProcessOutput.grabProcessOutput( |
| chattrProcess, |
| Wait.WAIT_FOR_READERS, |
| new IProcessOutput() { |
| @Override |
| public void out(@Nullable String line) { } |
| |
| @Override |
| public void err(@Nullable String line) { |
| // Don't complain if this command is not supported. That just means |
| // that the file system is not 'btrfs', and it does not support Copy |
| // on Write. So we're happy. |
| if (line != null && !line.startsWith("chattr: Operation not supported")) { |
| errorOutput.add(line); |
| } |
| } |
| }); |
| |
| if (!errorOutput.isEmpty()) { |
| log.warning("Failed 'chattr' for %1$s:", avdFolder.getAbsolutePath()); |
| for (String error : errorOutput) { |
| log.warning(" -- %1$s", error); |
| } |
| } |
| } |
| catch (InterruptedException | IOException ee) { |
| log.warning("Failed 'chattr' for %1$s: %2$s", avdFolder.getAbsolutePath(), ee); |
| } |
| } |
| } |