| /* |
| * 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 static com.google.common.collect.ImmutableList.toImmutableList; |
| import static java.util.stream.Collectors.joining; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.concurrency.GuardedBy; |
| 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.repository.api.ConsoleProgressIndicator; |
| import com.android.repository.api.LocalPackage; |
| import com.android.repository.api.ProgressIndicator; |
| 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.SystemImageTags; |
| 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.utils.FileUtils; |
| import com.android.utils.GrabProcessOutput; |
| import com.android.utils.GrabProcessOutput.IProcessOutput; |
| import com.android.utils.GrabProcessOutput.Wait; |
| import com.android.utils.ILogger; |
| import com.android.utils.PathUtils; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.ImmutableList; |
| 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.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.nio.charset.Charset; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.FileSystems; |
| import java.nio.file.Files; |
| import java.nio.file.NoSuchFileException; |
| 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.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.OptionalLong; |
| import java.util.Scanner; |
| 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 { |
| |
| /** |
| * 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 for the tag ids of the AVD's system image, represented as a |
| * comma-separated list |
| */ |
| public static final String AVD_INI_TAG_IDS = "tag.ids"; // $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 for the display names of the tags of the AVD's system image, |
| * represented as a comma-separated list |
| */ |
| public static final String AVD_INI_TAG_DISPLAYNAMES = "tag.displaynames"; // $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 resizable settings */ |
| public static final String AVD_INI_RESIZABLE_CONFIG = "hw.resizable.configs"; |
| |
| /** 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"; |
| |
| /** AVD/user-settings.ini key for Preferred ABI */ |
| public static final String USER_SETTINGS_INI_PREFERRED_ABI = "abi.type.preferred"; |
| |
| 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 Sdk Extension level of this AVD. Derived from the target hash. */ |
| public static final String AVD_INI_ANDROID_EXTENSION = "image.androidVersion.extension"; |
| |
| /** Whether the AVD's target Sdk Extension is the base extension */ |
| public static final String AVD_INI_ANDROID_IS_BASE_EXTENSION = |
| "image.androidVersion.isBaseExtension"; |
| |
| /** |
| * The API codename of this AVD. Derived from the target hash. |
| */ |
| public static final String AVD_INI_ANDROID_CODENAME = "image.androidVersion.codename"; |
| |
| public static final String AVD_INI_ANDROID_EXTENSION_LEVEL = "image.androidVersion.ext"; |
| |
| /** |
| * 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"; |
| public static final String USER_SETTINGS_INI = "user-settings.ini"; // $NON-NLS-1$ |
| |
| 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 static class AvdMgrException extends Exception {} |
| |
| @NonNull private final AndroidSdkHandler mSdkHandler; |
| |
| @NonNull private final Path mBaseAvdFolder; |
| |
| @NonNull private final ILogger mLog; |
| |
| @NonNull private final DeviceManager mDeviceManager; |
| |
| @GuardedBy("mAllAvdList") |
| private final ArrayList<AvdInfo> mAllAvdList = new ArrayList<>(); |
| |
| @GuardedBy("mAllAvdList") |
| private ImmutableList<AvdInfo> mValidAvdList; |
| |
| private AvdManager( |
| @NonNull AndroidSdkHandler sdkHandler, |
| @NonNull Path baseAvdFolder, |
| @NonNull DeviceManager deviceManager, |
| @NonNull ILogger log) |
| throws AndroidLocationsException { |
| if (sdkHandler.getLocation() == null) { |
| throw new AndroidLocationsException("Local SDK path not set!"); |
| } |
| mSdkHandler = sdkHandler; |
| mBaseAvdFolder = baseAvdFolder; |
| mLog = log; |
| mDeviceManager = deviceManager; |
| buildAvdList(mAllAvdList); |
| } |
| |
| @NonNull |
| public static AvdManager createInstance( |
| @NonNull AndroidSdkHandler sdkHandler, |
| @NonNull Path baseAvdFolder, |
| @NonNull DeviceManager deviceManager, |
| @NonNull ILogger log) |
| throws AndroidLocationsException { |
| return new AvdManager(sdkHandler, baseAvdFolder, deviceManager, log); |
| } |
| |
| |
| /** Returns the base folder where AVDs are created. */ |
| @NonNull |
| public Path 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. */ |
| @NonNull |
| public ImmutableList<AvdInfo> getValidAvds() { |
| synchronized (mAllAvdList) { |
| if (mValidAvdList == null) { |
| mValidAvdList = |
| mAllAvdList.stream() |
| .filter(avd -> avd.getStatus() == AvdStatus.OK) |
| .collect(toImmutableList()); |
| } |
| return mValidAvdList; |
| } |
| } |
| |
| /** |
| * Returns the {@link AvdInfo} matching the given <var>name</var>. |
| * |
| * <p>The search is case-insensitive on Windows. |
| * |
| * @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) { |
| String pid; |
| try { |
| pid = getAvdPid(info); |
| } |
| catch (IOException e) { |
| mLog.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) { |
| mLog.warning( |
| "Got IOException while checking running processes:\n%s", |
| Arrays.toString(e.getStackTrace())); |
| // To be safe return true |
| return true; |
| } catch (InterruptedException e) { |
| mLog.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) { |
| String pid; |
| try { |
| pid = getAvdPid(info); |
| } |
| catch (IOException ex) { |
| mLog.error(ex, "AVD not launched but got IOException while getting PID"); |
| return; |
| } |
| if (pid == null) { |
| mLog.warning( |
| "AVD not launched but PID is null. Should not have indicated that the AVD is running."); |
| return; |
| } |
| mLog.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) { |
| mLog.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) { |
| mLog.warning("Info for that AVD process is null"); |
| } |
| else { |
| mLog.warning( |
| "AVD process info: [" |
| + new String(procInfo, 0, nRead - numTermChars) |
| + "]"); |
| } |
| } |
| } |
| catch (IOException | InterruptedException ex) { |
| mLog.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) { |
| } |
| } |
| |
| @Slow |
| @NonNull |
| public OptionalLong getPid(@NonNull AvdInfo avd) { |
| OptionalLong pid = getPid(avd, "hardware-qemu.ini.lock"); |
| |
| if (pid.isPresent()) { |
| return pid; |
| } |
| |
| return getPid(avd, "userdata-qemu.img.lock"); |
| } |
| |
| @NonNull |
| private OptionalLong getPid(@NonNull AvdInfo avd, @NonNull String element) { |
| Path file = resolve(avd, element); |
| |
| try (Scanner scanner = new Scanner(file)) { |
| // TODO(http://b/233670812) |
| scanner.useDelimiter("\0"); |
| |
| return OptionalLong.of(scanner.nextLong()); |
| } catch (NoSuchFileException exception) { |
| mLog.info("%s not found for %s", file, avd.getName()); |
| return OptionalLong.empty(); |
| } catch (IOException | NoSuchElementException exception) { |
| mLog.error(exception, "avd = %s, file = %s", avd.getName(), file); |
| return OptionalLong.empty(); |
| } |
| } |
| |
| @VisibleForTesting |
| @NonNull |
| Path resolve(@NonNull AvdInfo avd, @NonNull String element) { |
| Path path = mBaseAvdFolder.resolve(avd.getDataFolderPath()).resolve(element); |
| |
| // path is a file on Linux and macOS. On Windows it's a directory. Return the path to the |
| // pid file under it. |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| return path.resolve("pid"); |
| } |
| |
| return path; |
| } |
| |
| /** @deprecated Use {@link #getPid(AvdInfo)} */ |
| @Deprecated |
| private String getAvdPid(@NonNull AvdInfo info) throws IOException { |
| Path dataFolderPath = mBaseAvdFolder.resolve(info.getDataFolderPath()); |
| // this is a file on Unix, and a directory on Windows. |
| Path f = dataFolderPath.resolve("hardware-qemu.ini.lock"); // $NON-NLS-1$ |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| f = f.resolve("pid"); |
| } |
| // This is an alternative identifier for Unix and Windows when the above one is missing. |
| Path alternative = dataFolderPath.resolve("userdata-qemu.img.lock"); |
| if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { |
| alternative = alternative.resolve("pid"); |
| } |
| try { |
| return CancellableFileIo.readString(f).trim(); |
| } catch (NoSuchFileException ignore) { |
| } |
| try { |
| return CancellableFileIo.readString(alternative).trim(); |
| } catch (NoSuchFileException ignore) { |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Reloads the AVD list. |
| * |
| * @throws AndroidLocationsException if there was an error finding the location of the AVD |
| * folder. |
| */ |
| @Slow |
| public void reloadAvds() throws AndroidLocationsException { |
| mSdkHandler.clearSystemImageManagerCache(); |
| // 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); |
| |
| synchronized (mAllAvdList) { |
| mAllAvdList.clear(); |
| mAllAvdList.addAll(allList); |
| mValidAvdList = null; |
| } |
| } |
| |
| /** |
| * Reloads a single AVD but does not update the list. |
| * |
| * @param avdInfo an existing AVD |
| * @return an updated AVD |
| */ |
| @Slow |
| public AvdInfo reloadAvd(@NonNull AvdInfo avdInfo) { |
| AvdInfo newInfo = parseAvdInfo(avdInfo.getIniFile()); |
| 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 userSettings optional settings for the AVD. Can be null. |
| * @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. |
| * @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> userSettings, |
| @Nullable Map<String, String> bootProps, |
| boolean deviceHasPlayStore, |
| boolean removePrevious, |
| boolean editExisting) { |
| Path 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(avdFolder, mLog); |
| // 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(avdFolder); |
| inhibitCopyOnWrite(avdFolder, mLog); |
| } |
| catch (SecurityException e) { |
| mLog.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(avdFolder, avdName, systemImage); |
| if (newAvdInfo == null) { |
| return null; |
| } |
| avdFolder = mBaseAvdFolder.resolve(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().toString())); |
| } |
| } |
| } |
| |
| // Write the AVD ini file |
| iniFile = |
| createAvdIniFile( |
| avdName, avdFolder, removePrevious, systemImage.getAndroidVersion()); |
| |
| needCleanup = true; |
| |
| createAvdUserdata(systemImage, avdFolder); |
| createAvdConfigFile(systemImage, configValues); |
| |
| // Tag and abi type |
| IdDisplay tag = systemImage.getTag(); |
| configValues.put(AVD_INI_TAG_ID, tag.getId()); |
| configValues.put(AVD_INI_TAG_DISPLAY, tag.getDisplay()); |
| List<IdDisplay> tags = systemImage.getTags(); |
| configValues.put( |
| AVD_INI_TAG_IDS, tags.stream().map(IdDisplay::getId).collect(joining(","))); |
| configValues.put( |
| AVD_INI_TAG_DISPLAYNAMES, |
| tags.stream().map(IdDisplay::getDisplay).collect(joining(","))); |
| configValues.put(AVD_INI_ABI_TYPE, systemImage.getPrimaryAbiType()); |
| configValues.put(AVD_INI_PLAYSTORE_ENABLED, Boolean.toString(deviceHasPlayStore && systemImage.hasPlayStore())); |
| configValues.put( |
| AVD_INI_ARC, Boolean.toString(SystemImageTags.CHROMEOS_TAG.equals(tag))); |
| |
| createAvdSkin(skinFolder, skinName, configValues); |
| createAvdSdCard(sdcard, editExisting, configValues, avdFolder); |
| |
| if (hardwareConfig == null) { |
| hardwareConfig = new HashMap<>(); |
| } |
| writeCpuArch(systemImage, hardwareConfig, mLog); |
| |
| addHardwareConfig(systemImage, skinFolder, avdFolder, hardwareConfig, configValues); |
| |
| if (userSettings != null) { |
| try { |
| writeIniFile(avdFolder.resolve(USER_SETTINGS_INI), userSettings, true); |
| } |
| catch (IOException e) { |
| mLog.warning("Could not write user settings file (at %1$s): %2$s", |
| avdFolder.resolve(USER_SETTINGS_INI).toString(), e); |
| } |
| } |
| |
| if (bootProps != null && !bootProps.isEmpty()) { |
| Path bootPropsFile = avdFolder.resolve(BOOT_PROP); |
| writeIniFile(bootPropsFile, bootProps, false); |
| } |
| |
| AvdInfo oldAvdInfo = getAvd(avdName, false /*validAvdOnly*/); |
| |
| if (newAvdInfo == null) { |
| newAvdInfo = |
| createAvdInfoObject( |
| systemImage, |
| avdName, |
| removePrevious, |
| editExisting, |
| iniFile, |
| avdFolder, |
| oldAvdInfo, |
| configValues, |
| userSettings); |
| } |
| |
| if ((removePrevious || editExisting) && |
| oldAvdInfo != null && |
| !oldAvdInfo.getDataFolderPath().equals(newAvdInfo.getDataFolderPath())) { |
| mLog.warning( |
| "Removing previous AVD directory at %s", oldAvdInfo.getDataFolderPath()); |
| // Remove the old data directory |
| try { |
| PathUtils.deleteRecursivelyIfExists( |
| mBaseAvdFolder.resolve(oldAvdInfo.getDataFolderPath())); |
| } catch (IOException exception) { |
| mLog.warning("Failed to delete %1$s: %2$s", oldAvdInfo.getDataFolderPath()); |
| } |
| } |
| |
| needCleanup = false; |
| return newAvdInfo; |
| } catch (AvdMgrException e) { |
| // Warning has already been logged |
| } catch (SecurityException | AndroidLocationsException | IOException e) { |
| mLog.warning("%1$s", e); |
| } finally { |
| if (needCleanup) { |
| if (iniFile != null) { |
| try { |
| PathUtils.deleteRecursivelyIfExists(iniFile); |
| } catch (IOException ignore) { |
| } |
| } |
| |
| try { |
| PathUtils.deleteRecursivelyIfExists(avdFolder); |
| } catch (Exception e) { |
| mLog.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 |
| */ |
| @Nullable |
| private AvdInfo duplicateAvd( |
| @NonNull Path origAvd, @NonNull String newAvdName, @NonNull ISystemImage systemImage) { |
| try { |
| Path destAvdFolder = origAvd.getParent().resolve(newAvdName + AVD_FOLDER_EXTENSION); |
| inhibitCopyOnWrite(destAvdFolder, mLog); |
| |
| ProgressIndicator progInd = new ConsoleProgressIndicator(); |
| progInd.setText("Copying files"); |
| progInd.setIndeterminate(true); |
| FileOpUtils.recursiveCopy(origAvd, |
| destAvdFolder, |
| false, |
| path -> !path.toString().endsWith(".lock"), // Do not copy *.lock files |
| progInd); |
| |
| // Modify the ID and display name in the new config.ini |
| Path configIni = destAvdFolder.resolve(CONFIG_INI); |
| Map<String, String> configVals = parseIniFile(new PathFileWrapper(configIni), mLog); |
| Map<String, String> userSettingsVals = |
| AvdInfo.parseUserSettingsFile(destAvdFolder, mLog); |
| 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.getFileName().toString().replace(".avd", ""); |
| String origAvdPath = origAvd.toAbsolutePath().toString(); |
| String newAvdPath = destAvdFolder.toAbsolutePath().toString(); |
| |
| configVals = |
| updateNameAndIniPaths( |
| configIni, origAvdName, origAvdPath, newAvdName, newAvdPath); |
| |
| Path hwQemu = destAvdFolder.resolve(HARDWARE_QEMU_INI); |
| updateNameAndIniPaths(hwQemu, origAvdName, origAvdPath, newAvdName, newAvdPath); |
| |
| // Create <AVD name>.ini |
| Path iniFile = |
| createAvdIniFile( |
| newAvdName, destAvdFolder, false, systemImage.getAndroidVersion()); |
| |
| // Create an AVD object from these files |
| return new AvdInfo( |
| newAvdName, iniFile, destAvdFolder, systemImage, configVals, userSettingsVals); |
| } catch (AndroidLocationsException | IOException e) { |
| mLog.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 Path iniFile, |
| @NonNull String oldName, |
| @NonNull String oldPath, |
| @NonNull String newName, |
| @NonNull String newPath) |
| throws IOException { |
| Map<String, String> iniVals = parseIniFile(new PathFileWrapper(iniFile), mLog); |
| 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 |
| String separator = folder.getFileSystem().getSeparator(); |
| if (imageFullPath.startsWith(separator)) { |
| imageFullPath = imageFullPath.substring(separator.length()); |
| } |
| // For compatibility with previous versions, we denote folders |
| // by ending the path with file separator |
| if (!imageFullPath.endsWith(separator)) { |
| imageFullPath += 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 Files#delete(Path)} ()} fails. |
| */ |
| private Path createAvdIniFile( |
| @NonNull String name, |
| @NonNull Path avdFolder, |
| boolean removePrevious, |
| @NonNull AndroidVersion version) |
| throws AndroidLocationsException, IOException { |
| Path iniFile = AvdInfo.getDefaultIniFile(this, name); |
| |
| if (removePrevious) { |
| if (CancellableFileIo.isRegularFile(iniFile)) { |
| Files.delete(iniFile); |
| } else if (CancellableFileIo.isDirectory(iniFile)) { |
| try { |
| PathUtils.deleteRecursivelyIfExists(iniFile); |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| |
| String absPath = avdFolder.toAbsolutePath().toString(); |
| 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() + File.separator; |
| if (absPath.startsWith(androidPath)) { |
| // Compute the AVD path relative to the android path. |
| 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 Files#delete(Path)} fails. |
| */ |
| private Path createAvdIniFile(@NonNull AvdInfo info) |
| throws AndroidLocationsException, IOException { |
| return createAvdIniFile( |
| info.getName(), |
| mBaseAvdFolder.resolve(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 |
| * @return True if the AVD was deleted with no error. |
| */ |
| @Slow |
| public boolean deleteAvd(@NonNull AvdInfo avdInfo) { |
| try { |
| boolean error = false; |
| |
| Path f = avdInfo.getIniFile(); |
| try { |
| Files.deleteIfExists(f); |
| } catch (IOException exception) { |
| mLog.warning("Failed to delete %1$s\n", f); |
| error = true; |
| } |
| |
| Path path = avdInfo.getDataFolderPath(); |
| f = mBaseAvdFolder.resolve(path); |
| try { |
| PathUtils.deleteRecursivelyIfExists(f); |
| } catch (IOException exception) { |
| mLog.warning("Failed to delete %1$s\n", f); |
| StringWriter writer = new StringWriter(); |
| exception.printStackTrace(new PrintWriter(writer)); |
| mLog.warning(writer.toString()); |
| error = true; |
| } |
| |
| removeAvd(avdInfo); |
| |
| if (error) { |
| mLog.info( |
| "\nAVD '%1$s' deleted with errors. See errors above.\n", avdInfo.getName()); |
| } else { |
| mLog.info("\nAVD '%1$s' deleted.\n", avdInfo.getName()); |
| return true; |
| } |
| |
| } catch (SecurityException e) { |
| mLog.warning("%1$s", e); |
| } |
| return false; |
| } |
| |
| /** |
| * Moves and/or renames 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. |
| * @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 Path paramFolderPath) { |
| try { |
| if (paramFolderPath != null) { |
| Path f = mBaseAvdFolder.resolve(avdInfo.getDataFolderPath()); |
| mLog.info( |
| "Moving '%1$s' to '%2$s'.\n", avdInfo.getDataFolderPath(), paramFolderPath); |
| try { |
| Files.move(f, mBaseAvdFolder.resolve(paramFolderPath)); |
| } catch (IOException exception) { |
| mLog.error( |
| exception, |
| "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(), |
| avdInfo.getUserSettings()); |
| replaceAvd(avdInfo, info); |
| |
| // update the ini file |
| createAvdIniFile(info); |
| } |
| |
| if (newName != null) { |
| Path oldIniFile = avdInfo.getIniFile(); |
| Path newIniFile = AvdInfo.getDefaultIniFile(this, newName); |
| |
| mLog.warning("Moving '%1$s' to '%2$s'.", oldIniFile, newIniFile); |
| try { |
| Files.move(oldIniFile, newIniFile); |
| } catch (IOException exception) { |
| mLog.warning(null, "Failed to move '%1$s' to '%2$s'.", oldIniFile, newIniFile); |
| return false; |
| } |
| |
| // update AVD info |
| AvdInfo info = |
| new AvdInfo( |
| newName, |
| avdInfo.getIniFile(), |
| avdInfo.getDataFolderPath(), |
| avdInfo.getSystemImage(), |
| avdInfo.getProperties(), |
| avdInfo.getUserSettings()); |
| replaceAvd(avdInfo, info); |
| } |
| |
| mLog.info("AVD '%1$s' moved.\n", avdInfo.getName()); |
| |
| } catch (AndroidLocationsException | IOException e) { |
| mLog.warning("$1%s", e); |
| return false; |
| } |
| |
| // nothing to do or succeeded |
| return true; |
| } |
| |
| /** |
| * Recursively deletes 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(Path folder) throws SecurityException { |
| boolean success = true; |
| try (DirectoryStream<Path> entries = Files.newDirectoryStream(folder)) { |
| for (Path entry : entries) { |
| try { |
| PathUtils.deleteRecursivelyIfExists(entry); |
| } catch (IOException ignore) { |
| success = false; |
| } |
| } |
| } catch (IOException exception) { |
| return false; |
| } |
| return success; |
| } |
| |
| /** |
| * 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 Path} array or null. The array might be empty. |
| * @throws AndroidLocationsException if there's a problem getting android root directory. |
| */ |
| private Path[] buildAvdFilesList() throws AndroidLocationsException { |
| // ensure folder validity. |
| if (CancellableFileIo.isRegularFile(mBaseAvdFolder)) { |
| throw new AndroidLocationsException( |
| String.format("%1$s is not a valid folder.", mBaseAvdFolder.toAbsolutePath())); |
| } else if (CancellableFileIo.notExists(mBaseAvdFolder)) { |
| // folder is not there, we create it and return |
| try { |
| Files.createDirectories(mBaseAvdFolder); |
| } catch (IOException ignore) { |
| } |
| return null; |
| } |
| |
| Path[] avds = new Path[0]; |
| try (Stream<Path> contents = CancellableFileIo.list(mBaseAvdFolder)) { |
| avds = |
| contents.filter( |
| path -> { |
| if (INI_NAME_PATTERN |
| .matcher(path.getFileName().toString()) |
| .matches()) { |
| // check it's a file and not a folder |
| return Files.isRegularFile(path); |
| } |
| |
| return false; |
| }) |
| .toArray(Path[]::new); |
| } catch (IOException ignore) { |
| } |
| return avds; |
| } |
| |
| /** |
| * Computes the internal list of available AVDs |
| * |
| * @param allList the list to contain all the AVDs |
| * @throws AndroidLocationsException if there's a problem getting android root directory. |
| */ |
| private void buildAvdList(ArrayList<AvdInfo> allList) throws AndroidLocationsException { |
| Path[] avds = buildAvdFilesList(); |
| if (avds != null) { |
| for (Path avd : avds) { |
| AvdInfo info = parseAvdInfo(avd); |
| if (info != null && !allList.contains(info)) { |
| allList.add(info); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Parses an AVD .ini file to create an {@link AvdInfo}. |
| * |
| * @param iniPath The path to the AVD .ini file |
| * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is valid |
| * or not. |
| */ |
| @VisibleForTesting |
| @Slow |
| public AvdInfo parseAvdInfo(@NonNull Path iniPath) { |
| Map<String, String> map = parseIniFile(new PathFileWrapper(iniPath), mLog); |
| |
| Path avdPath = null; |
| if (map != null) { |
| String path = map.get(AVD_INFO_ABS_PATH); |
| avdPath = path == null ? null : iniPath.resolve(path); |
| if (avdPath == null |
| || !(CancellableFileIo.isDirectory(mBaseAvdFolder.resolve(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.toCompatiblePath(relPath) |
| : androidFolder.resolve(relPath); |
| if (CancellableFileIo.isDirectory(f)) { |
| avdPath = f; |
| } |
| } |
| } |
| } |
| if (avdPath == null || !(CancellableFileIo.isDirectory(mBaseAvdFolder.resolve(avdPath)))) { |
| // Corrupted .ini file |
| String avdName = iniPath.getFileName().toString(); |
| if (avdName.endsWith(".ini")) { |
| avdName = avdName.substring(0, avdName.length() - 4); |
| } |
| return new AvdInfo( |
| avdName, iniPath, iniPath, null, null, null, AvdStatus.ERROR_CORRUPTED_INI); |
| } |
| |
| PathFileWrapper configIniFile; |
| Map<String, String> properties = null; |
| LoggerProgressIndicatorWrapper progress = |
| new LoggerProgressIndicatorWrapper(mLog) { |
| @Override |
| public void logVerbose(@NonNull String s) { |
| // Skip verbose messages } |
| } |
| }; |
| |
| // load the AVD properties. |
| configIniFile = new PathFileWrapper(mBaseAvdFolder.resolve(avdPath).resolve(CONFIG_INI)); |
| |
| if (!configIniFile.exists()) { |
| mLog.warning("Missing file '%1$s'.", configIniFile.getOsLocation()); |
| } else { |
| properties = parseIniFile(configIniFile, mLog); |
| } |
| |
| // get name |
| String name = iniPath.getFileName().toString(); |
| Matcher matcher = INI_NAME_PATTERN.matcher(name); |
| 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 |
| ? mBaseAvdFolder.resolve(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) { |
| d = mDeviceManager.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 = new HashMap<>(); |
| } |
| |
| if (!properties.containsKey(AVD_INI_ANDROID_API) |
| && !properties.containsKey(AVD_INI_ANDROID_CODENAME)) { |
| String targetHash = map.get(AVD_INFO_TARGET); |
| if (targetHash != null) { |
| AndroidVersion version = AndroidTargetHash.getVersionFromHash(targetHash); |
| if (version != null) { |
| properties.put(AVD_INI_ANDROID_API, Integer.toString(version.getApiLevel())); |
| if (version.getExtensionLevel() != null) { |
| properties.put( |
| AVD_INI_ANDROID_EXTENSION, |
| Integer.toString(version.getExtensionLevel())); |
| properties.put( |
| AVD_INI_ANDROID_IS_BASE_EXTENSION, |
| Boolean.toString(version.isBaseExtension())); |
| } |
| if (version.getCodename() != null) { |
| properties.put(AVD_INI_ANDROID_CODENAME, version.getCodename()); |
| } |
| if (!version.isBaseExtension() && version.getExtensionLevel() != null) { |
| properties.put( |
| AVD_INI_ANDROID_EXTENSION_LEVEL, |
| Integer.toString(version.getExtensionLevel())); |
| } |
| } |
| } |
| } |
| |
| Map<String, String> userSettings = AvdInfo.parseUserSettingsFile(avdPath, mLog); |
| |
| AvdInfo info = |
| new AvdInfo(name, iniPath, avdPath, sysImage, properties, userSettings, status); |
| |
| if (updateHashV2) { |
| try { |
| return updateDeviceChanged(info); |
| } 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(Path iniFile, Map<String, String> values, boolean addEncoding) |
| throws IOException { |
| |
| Charset charset = Charsets.UTF_8; |
| try (OutputStreamWriter writer = |
| new OutputStreamWriter(Files.newOutputStream(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_EXTENSION); |
| keys.remove(AVD_INI_ANDROID_IS_BASE_EXTENSION); |
| 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 logger 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 logger) { |
| return parseIniFileImpl(propFile, logger, 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; |
| 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. |
| * @return True if the sdcard could be created. |
| */ |
| @VisibleForTesting |
| protected boolean createSdCard(String toolLocation, String size, String location) { |
| 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) { |
| mLog.warning("%1$s", error); |
| } |
| } |
| |
| } catch (InterruptedException | IOException e) { |
| // pass, print error below |
| } |
| |
| mLog.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 = null; |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| @Slow |
| public AvdInfo updateAvd(AvdInfo avd, Map<String, String> newProperties) throws IOException { |
| // now write the config file |
| Path configIniFile = mBaseAvdFolder.resolve(avd.getDataFolderPath()).resolve(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, |
| avd.getUserSettings()); |
| |
| replaceAvd(avd, newAvd); |
| |
| return newAvd; |
| } |
| |
| /** |
| * Updates the device-specific part of an AVD ini. |
| * |
| * @param avd the AVD to update. |
| * @return The new AVD on success. |
| */ |
| @Slow |
| public AvdInfo updateDeviceChanged(AvdInfo avd) throws IOException { |
| // Overwrite the properties derived from the device and nothing else |
| Map<String, String> properties = new HashMap<>(avd.getProperties()); |
| |
| Collection<Device> devices = mDeviceManager.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) { |
| mLog.warning("%1$s", e); |
| } |
| } |
| } |
| } else { |
| mLog.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. |
| * @return true if success, false if some path are missing. |
| */ |
| private boolean setImagePathProperties(ISystemImage image, Map<String, String> properties) { |
| 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) { |
| mLog.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 = null; |
| } |
| } |
| |
| /** |
| * Create the user data file for an AVD |
| * |
| * @param systemImage the system image of the AVD |
| * @param avdFolder where the AVDs live |
| */ |
| private void createAvdUserdata(@NonNull ISystemImage systemImage, @NonNull Path avdFolder) |
| 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.getPrimaryAbiType(); |
| |
| 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; |
| } |
| mLog.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)) { |
| FileUtils.copyFile(userdataSrc, userdataDest); |
| |
| if (CancellableFileIo.notExists(userdataDest)) { |
| mLog.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 |
| */ |
| private void createAvdConfigFile( |
| @NonNull ISystemImage systemImage, @Nullable HashMap<String, String> values) |
| throws AvdMgrException { |
| |
| if (!setImagePathProperties(systemImage, values)) { |
| mLog.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.getPrimaryAbiType(); |
| 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) |
| && SystemImageTags.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(); |
| } |
| } |
| |
| /** |
| * Links 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 |
| */ |
| private void createAvdSkin( |
| @Nullable Path skinFolder, |
| @Nullable String skinName, |
| @NonNull Map<String, String> values) |
| 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.getFileName().toString(); |
| } |
| |
| if (skinFolder != null) { |
| // skin does not exist! |
| if (CancellableFileIo.notExists(skinFolder)) { |
| mLog.warning("Skin '%1$s' does not exist at %2$s.", skinName, skinFolder); |
| throw new AvdMgrException(); |
| } |
| |
| // if skinFolder is in the sdk, use the relative path |
| if (skinFolder.startsWith(mSdkHandler.getLocation())) { |
| skinPath = mSdkHandler.getLocation().relativize(skinFolder).toString(); |
| } else { |
| // Skin isn't in the sdk. Just use the absolute path. |
| skinPath = skinFolder.toAbsolutePath().toString(); |
| } |
| } |
| |
| // 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); |
| } |
| } |
| |
| /** |
| * Creates 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 |
| */ |
| private void createAvdSdCard( |
| @Nullable String sdcard, |
| boolean editExisting, |
| @NonNull Map<String, String> values, |
| @NonNull Path avdFolder) |
| 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) { |
| mLog.warning("SD Card size must be in the range 9 MiB..1023 GiB."); |
| throw new AvdMgrException(); |
| } |
| |
| if (sdcardSize == SDCARD_SIZE_INVALID) { |
| mLog.warning("Unable to parse SD Card size"); |
| throw new AvdMgrException(); |
| } |
| |
| if (sdcardSize == SDCARD_NOT_SIZE_PATTERN) { |
| Path sdcardFile = mBaseAvdFolder.resolve(sdcard); |
| if (!CancellableFileIo.isRegularFile(sdcardFile)) { |
| mLog.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. |
| Path sdcardFile = avdFolder.resolve(SDCARD_IMG); |
| |
| boolean runMkSdcard = true; |
| try { |
| if (CancellableFileIo.size(sdcardFile) == 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; |
| mLog.info("SD Card already present with same size, was not changed.\n"); |
| } |
| } catch (NoSuchFileException ignore) { |
| } catch (IOException exception) { |
| AvdMgrException wrapper = new AvdMgrException(); |
| wrapper.initCause(exception); |
| throw wrapper; |
| } |
| if (!mBaseAvdFolder.getFileSystem().equals(FileSystems.getDefault())) { |
| // 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.toAbsolutePath().toString(); |
| |
| // execute mksdcard with the proper parameters. |
| LoggerProgressIndicatorWrapper progress = |
| new LoggerProgressIndicatorWrapper(mLog) { |
| @Override |
| public void logVerbose(@NonNull String s) { |
| // Skip verbose messages |
| } |
| }; |
| 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)) { |
| mLog.warning( |
| "'%1$s' is missing from the SDK tools folder.", mkSdCard.getFileName()); |
| throw new AvdMgrException(); |
| } |
| |
| if (!createSdCard(mkSdCard.toAbsolutePath().toString(), sdcard, path)) { |
| // mksdcard output has already been displayed, no need to |
| // output anything else. |
| mLog.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 |
| */ |
| private void addHardwareConfig( |
| @NonNull ISystemImage systemImage, |
| @Nullable Path skinFolder, |
| @NonNull Path avdFolder, |
| @Nullable Map<String, String> hardwareConfig, |
| @Nullable Map<String, String> values) |
| 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, mLog); |
| |
| 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, mLog); |
| |
| 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(configIniFile, values, true); |
| } |
| |
| /** |
| * Creates 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 Path iniFile, |
| @NonNull Path avdFolder, |
| @Nullable AvdInfo oldAvdInfo, |
| @Nullable Map<String, String> values, |
| @Nullable Map<String, String> userSettings) |
| throws AvdMgrException { |
| |
| // create the AvdInfo object, and add it to the list |
| AvdInfo theAvdInfo = |
| new AvdInfo(avdName, iniFile, avdFolder, systemImage, values, userSettings); |
| |
| synchronized (mAllAvdList) { |
| if (oldAvdInfo != null && (removePrevious || editExisting)) { |
| mAllAvdList.remove(oldAvdInfo); |
| } |
| mAllAvdList.add(theAvdInfo); |
| mValidAvdList = null; |
| } |
| return theAvdInfo; |
| } |
| |
| /** |
| * (Linux only) Sets the AVD folder to not be "Copy on Write" |
| * |
| * <p>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 Path 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.toAbsolutePath().toString(); |
| 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.toAbsolutePath().toString()); |
| for (String error : errorOutput) { |
| log.warning(" -- %1$s", error); |
| } |
| } |
| } |
| catch (InterruptedException | IOException ee) { |
| log.warning( |
| "Failed 'chattr' for %1$s: %2$s", avdFolder.toAbsolutePath().toString(), ee); |
| } |
| } |
| } |