com.android.tools.idea.avdmanager.AvdManagerConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.avdmanager.AvdManagerConnection.java

Source

/*
 * Copyright (C) 2014 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.tools.idea.avdmanager;

import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.IDevice;
import com.android.prefs.AndroidLocation;
import com.android.repository.Revision;
import com.android.repository.api.LocalPackage;
import com.android.repository.api.ProgressIndicator;
import com.android.repository.api.RepoPackage;
import com.android.repository.io.FileOp;
import com.android.repository.io.FileOpUtils;
import com.android.resources.ScreenOrientation;
import com.android.sdklib.ISystemImage;
import com.android.sdklib.devices.Abi;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.Storage;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.avd.HardwareProperties;
import com.android.sdklib.repository.AndroidSdkHandler;
import com.android.sdklib.repository.IdDisplay;
import com.android.sdklib.repository.targets.SystemImage;
import com.android.tools.idea.run.EmulatorConnectionListener;
import com.android.tools.idea.sdk.AndroidSdks;
import com.android.tools.idea.sdk.progress.StudioLoggerProgressIndicator;
import com.android.utils.ILogger;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingAnsiEscapesAwareProcessHandler;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
import com.intellij.openapi.progress.util.ProgressWindow;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.WeakHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.android.sdklib.repository.targets.SystemImage.DEFAULT_TAG;
import static com.android.sdklib.repository.targets.SystemImage.GOOGLE_APIS_TAG;

/**
 * A wrapper class for communicating with {@link AvdManager} and exposing helper functions
 * for dealing with {@link AvdInfo} objects inside Android studio.
 */
public class AvdManagerConnection {
    private static final Logger IJ_LOG = Logger.getInstance(AvdManagerConnection.class);
    private static final ILogger SDK_LOG = new LogWrapper(IJ_LOG);
    private static final ProgressIndicator REPO_LOG = new StudioLoggerProgressIndicator(AvdManagerConnection.class);
    private static final AvdManagerConnection NULL_CONNECTION = new AvdManagerConnection(null);
    private static final int MNC_API_LEVEL_23 = 23;
    private static final int LMP_MR1_API_LEVEL_22 = 22;

    public static final String AVD_INI_HW_LCD_DENSITY = "hw.lcd.density";
    public static final Revision TOOLS_REVISION_WITH_FIRST_QEMU2 = Revision.parseRevision("25.0.0 rc1");
    public static final Revision TOOLS_REVISION_25_0_2_RC3 = Revision.parseRevision("25.0.2 rc3");
    public static final Revision PLATFORM_TOOLS_REVISION_WITH_FIRST_QEMU2 = Revision.parseRevision("23.1.0");

    private static final SystemImageUpdateDependency[] SYSTEM_IMAGE_DEPENCENCY_WITH_FIRST_QEMU2 = {
            new SystemImageUpdateDependency(LMP_MR1_API_LEVEL_22, DEFAULT_TAG, 2),
            new SystemImageUpdateDependency(LMP_MR1_API_LEVEL_22, GOOGLE_APIS_TAG, 2),
            new SystemImageUpdateDependency(MNC_API_LEVEL_23, DEFAULT_TAG, 6),
            new SystemImageUpdateDependency(MNC_API_LEVEL_23, GOOGLE_APIS_TAG, 10), };
    private static final SystemImageUpdateDependency[] SYSTEM_IMAGE_DEPENCENCY_WITH_25_0_2_RC3 = {
            new SystemImageUpdateDependency(LMP_MR1_API_LEVEL_22, DEFAULT_TAG, 4),
            new SystemImageUpdateDependency(LMP_MR1_API_LEVEL_22, GOOGLE_APIS_TAG, 4),
            new SystemImageUpdateDependency(MNC_API_LEVEL_23, DEFAULT_TAG, 8),
            new SystemImageUpdateDependency(MNC_API_LEVEL_23, GOOGLE_APIS_TAG, 12), };

    private AvdManager myAvdManager;
    private static Map<File, AvdManagerConnection> ourCache = new WeakHashMap<>();
    private static long ourMemorySize = -1;
    private final FileOp myFileOp;

    private static Function<AndroidSdkHandler, AvdManagerConnection> ourConnectionFactory = AvdManagerConnection::new;

    @Nullable
    private final AndroidSdkHandler mySdkHandler;

    @NotNull
    public static AvdManagerConnection getDefaultAvdManagerConnection() {
        AndroidSdkHandler handler = AndroidSdks.getInstance().tryToChooseSdkHandler();
        if (handler.getLocation() == null) {
            return NULL_CONNECTION;
        } else {
            return getAvdManagerConnection(handler);
        }
    }

    @NotNull
    public synchronized static AvdManagerConnection getAvdManagerConnection(@NotNull AndroidSdkHandler handler) {
        File sdkPath = handler.getLocation();
        if (!ourCache.containsKey(sdkPath)) {
            ourCache.put(sdkPath, ourConnectionFactory.apply(handler));
        }
        return ourCache.get(sdkPath);
    }

    @VisibleForTesting
    public AvdManagerConnection(@Nullable AndroidSdkHandler handler) {
        mySdkHandler = handler;
        myFileOp = handler == null ? FileOpUtils.create() : handler.getFileOp();
    }

    /**
     * Sets a factory to be used for creating connections, so subclasses can be injected for testing.
     */
    @VisibleForTesting
    protected synchronized static void setConnectionFactory(
            Function<AndroidSdkHandler, AvdManagerConnection> factory) {
        ourCache.clear();
        ourConnectionFactory = factory;
    }

    /**
     * Setup our static instances if required. If the instance already exists, then this is a no-op.
     */
    private boolean initIfNecessary() {
        if (myAvdManager == null) {
            if (mySdkHandler == null) {
                IJ_LOG.warn("No Android SDK Found");
                return false;
            }
            try {
                myAvdManager = AvdManager.getInstance(mySdkHandler, new File(AndroidLocation.getAvdFolder()),
                        SDK_LOG, myFileOp);
            } catch (AndroidLocation.AndroidLocationException e) {
                IJ_LOG.error("Could not instantiate AVD Manager from SDK", e);
                return false;
            }
            if (myAvdManager == null) {
                return false;
            }
        }
        return true;
    }

    private File getBinaryLocation(String filename) {
        assert mySdkHandler != null;
        LocalPackage sdkPackage = mySdkHandler.getLocalPackage(SdkConstants.FD_EMULATOR, REPO_LOG);
        if (sdkPackage == null) {
            sdkPackage = mySdkHandler.getLocalPackage(SdkConstants.FD_TOOLS, REPO_LOG);
        }
        if (sdkPackage != null) {
            return new File(sdkPackage.getLocation(), filename);
        }
        // Fallback to old behavior, in the weird case nothing is installed.
        return new File(mySdkHandler.getLocation(), FileUtil.join(SdkConstants.OS_SDK_TOOLS_FOLDER, filename));
    }

    private File getEmulatorBinary() {
        return getBinaryLocation(SdkConstants.FN_EMULATOR);
    }

    public File getEmulatorCheckBinary() {
        return getBinaryLocation(SdkConstants.FN_EMULATOR_CHECK);
    }

    /**
     * Return the SystemImageUpdateDependencies for the current emulator
     * or null if no emulator is installed or if the emulator is not an qemu2 emulator.
     */
    @Nullable
    private SystemImageUpdateDependency[] getSystemImageUpdateDependencies() {
        assert mySdkHandler != null;
        LocalPackage info = mySdkHandler.getSdkManager(REPO_LOG).getPackages().getLocalPackages()
                .get(SdkConstants.FD_TOOLS);
        if (info == null) {
            return null;
        }
        if (info.getVersion().compareTo(TOOLS_REVISION_25_0_2_RC3) >= 0) {
            return SYSTEM_IMAGE_DEPENCENCY_WITH_25_0_2_RC3;
        }
        if (info.getVersion().compareTo(TOOLS_REVISION_WITH_FIRST_QEMU2) >= 0) {
            return SYSTEM_IMAGE_DEPENCENCY_WITH_FIRST_QEMU2;
        }
        return null;
    }

    private boolean hasQEMU2Installed() {
        return getSystemImageUpdateDependencies() != null;
    }

    private boolean hasPlatformToolsForQEMU2Installed() {
        assert mySdkHandler != null;
        LocalPackage info = mySdkHandler.getSdkManager(REPO_LOG).getPackages().getLocalPackages()
                .get(SdkConstants.FD_PLATFORM_TOOLS);
        if (info == null) {
            return false;
        }
        if (info.getVersion().compareTo(PLATFORM_TOOLS_REVISION_WITH_FIRST_QEMU2) >= 0) {
            return true;
        }
        return false;
    }

    private boolean hasSystemImagesForQEMU2Installed() {
        return getSystemImageUpdates().isEmpty();
    }

    /**
     * The qemu2 emulator has changes in the system images for platform 22 and 23 (Intel CPU architecture only).
     * This method will generate package updates if we detect that we have outdated system images for platform
     * 22 and 23. We also check the addon system images which includes the Google API.
     * @return a list of package paths that need to be updated.
     */
    @NotNull
    public List<String> getSystemImageUpdates() {
        List<String> requested = Lists.newArrayList();
        SystemImageUpdateDependency[] dependencies = getSystemImageUpdateDependencies();
        if (dependencies == null) {
            return requested;
        }

        assert mySdkHandler != null;
        for (SystemImage systemImage : mySdkHandler.getSystemImageManager(REPO_LOG).getImages()) {
            for (SystemImageUpdateDependency dependency : dependencies) {
                if (dependency.updateRequired(systemImage)) {
                    requested.add(systemImage.getPackage().getPath());
                    break;
                }
            }
        }
        return requested;
    }

    /**
     * @param forceRefresh if true the manager will read the AVD list from disk. If false, the cached version in memory
     *                     is returned if available
     * @return a list of AVDs currently present on the system.
     */
    @NotNull
    public List<AvdInfo> getAvds(boolean forceRefresh) {
        if (!initIfNecessary()) {
            return ImmutableList.of();
        }
        if (forceRefresh) {
            try {
                myAvdManager.reloadAvds(SDK_LOG);
            } catch (AndroidLocation.AndroidLocationException e) {
                IJ_LOG.error("Could not find Android SDK!", e);
            }
        }
        ArrayList<AvdInfo> avdInfos = Lists.newArrayList(myAvdManager.getAllAvds());
        boolean needsRefresh = false;
        for (AvdInfo info : avdInfos) {
            if (info.getStatus() == AvdInfo.AvdStatus.ERROR_DEVICE_CHANGED) {
                updateDeviceChanged(info);
                needsRefresh = true;
            }
        }
        if (needsRefresh) {
            return getAvds(true);
        } else {
            return avdInfos;
        }
    }

    public boolean deleteAvd(@NotNull String avdName) {
        if (!initIfNecessary()) {
            return false;
        }
        AvdInfo info = myAvdManager.getAvd(avdName, false);
        if (info == null) {
            return false;
        }
        return deleteAvd(info);
    }

    /**
     * Delete the given AVD if it exists.
     */
    public boolean deleteAvd(@NotNull AvdInfo info) {
        if (!initIfNecessary()) {
            return false;
        }
        return myAvdManager.deleteAvd(info, SDK_LOG);
    }

    public boolean isAvdRunning(@NotNull AvdInfo info) {
        return myAvdManager.isAvdRunning(info, SDK_LOG);
    }

    public void stopAvd(@NotNull final AvdInfo info) {
        myAvdManager.stopAvd(info);
    }

    /**
     * Launch the given AVD in the emulator.
     * @return a future with the device that was launched
     */
    @NotNull
    public ListenableFuture<IDevice> startAvd(@Nullable final Project project, @NotNull final AvdInfo info) {
        if (!initIfNecessary()) {
            return Futures.immediateFailedFuture(new RuntimeException("No Android SDK Found"));
        }
        AccelerationErrorCode error = checkAcceleration();
        ListenableFuture<IDevice> errorResult = handleAccelerationError(project, info, error);
        if (errorResult != null) {
            return errorResult;
        }

        final File emulatorBinary = getEmulatorBinary();
        if (!emulatorBinary.isFile()) {
            IJ_LOG.error("No emulator binary found!");
            return Futures.immediateFailedFuture(new RuntimeException("No emulator binary found"));
        }

        final String avdName = info.getName();

        // TODO: The emulator stores pid of the running process inside the .lock file (userdata-qemu.img.lock in Linux and
        // userdata-qemu.img.lock/pid on Windows). We should detect whether those lock files are stale and if so, delete them without showing
        // this error. Either the emulator provides a command to do that, or we learn about its internals (qemu/android/utils/filelock.c) and
        // perform the same action here. If it is not stale, then we should show this error and if possible, bring that window to the front.
        if (myAvdManager.isAvdRunning(info, SDK_LOG)) {
            String baseFolder;
            try {
                baseFolder = myAvdManager.getBaseAvdFolder().getAbsolutePath();
            } catch (AndroidLocation.AndroidLocationException e) {
                baseFolder = "$HOME";
            }

            String message = String
                    .format("AVD %1$s is already running.\n" + "If that is not the case, delete the files at\n"
                            + "   %2$s/%1$s.avd/*.lock\n" + "and try again.", avdName, baseFolder);
            Messages.showErrorDialog(project, message, "AVD Manager");
            return Futures.immediateFailedFuture(new RuntimeException(message));
        }

        GeneralCommandLine commandLine = new GeneralCommandLine();
        commandLine.setExePath(emulatorBinary.getPath());

        addParameters(info, commandLine);

        EmulatorRunner runner = new EmulatorRunner(commandLine, info);
        EmulatorRunner.ProcessOutputCollector collector = new EmulatorRunner.ProcessOutputCollector();
        runner.addProcessListener(collector);

        final ProcessHandler processHandler;
        try {
            processHandler = runner.start();
        } catch (ExecutionException e) {
            IJ_LOG.error("Error launching emulator", e);
            return Futures.immediateFailedFuture(
                    new RuntimeException(String.format("Error launching emulator %1$s ", avdName), e));
        }

        // If we're using qemu2, it has its own progress bar, so put ours in the background. Otherwise show it.
        final ProgressWindow p = hasQEMU2Installed()
                ? new BackgroundableProcessIndicator(project, "Launching Emulator",
                        PerformInBackgroundOption.ALWAYS_BACKGROUND, "", "", false)
                : new ProgressWindow(false, true, project);
        p.setIndeterminate(false);
        p.setDelayInMillis(0);

        // It takes >= 8 seconds to start the Emulator. Display a small progress indicator otherwise it seems like
        // the action wasn't invoked and users tend to click multiple times on it, ending up with several instances of the emulator
        ApplicationManager.getApplication().executeOnPooledThread(() -> {
            try {
                p.start();
                p.setText("Starting AVD...");
                for (double d = 0; d < 1; d += 1.0 / 80) {
                    p.setFraction(d);
                    //noinspection BusyWait
                    Thread.sleep(100);
                    if (processHandler.isProcessTerminated()) {
                        break;
                    }
                }
            } catch (InterruptedException ignore) {
            } finally {
                p.stop();
                p.processFinish();
            }

            processHandler.removeProcessListener(collector);
            String message = limitErrorMessage(collector.getText());

            if (message.toLowerCase(Locale.ROOT).contains("error")
                    || processHandler.isProcessTerminated() && !message.trim().isEmpty()) {
                ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog(project,
                        "Cannot launch AVD in emulator.\nOutput:\n" + message, avdName));
            }
        });

        return EmulatorConnectionListener.getDeviceForEmulator(project, info.getName(), processHandler, 5,
                TimeUnit.MINUTES);
    }

    /**
     * Limit the error message retrieved from the emulator to the smaller of 1K characters or 30 lines.
     */
    private static String limitErrorMessage(@NotNull String message) {
        int offset = StringUtil.lineColToOffset(message, 30, 0);
        if (offset < 0) {
            offset = message.length();
        }
        return message.substring(0, Math.min(offset, 1024));
    }

    /**
     * Adds necessary parameters to {@code commandLine}.
     */
    protected void addParameters(@NotNull AvdInfo info, GeneralCommandLine commandLine) {
        Map<String, String> properties = info.getProperties();
        String netDelay = properties.get(AvdWizardUtils.AVD_INI_NETWORK_LATENCY);
        String netSpeed = properties.get(AvdWizardUtils.AVD_INI_NETWORK_SPEED);
        if (netDelay != null) {
            commandLine.addParameters("-netdelay", netDelay);
        }

        if (netSpeed != null) {
            commandLine.addParameters("-netspeed", netSpeed);
        }

        commandLine.addParameters("-avd", info.getName());
    }

    /**
     * Handle the {@link AccelerationErrorCode} found when attempting to start an AVD.
     * @param project
     * @param error
     * @return a future with a device that was launched delayed, or null if startAvd should proceed to start the AVD.
     */
    @Nullable
    private ListenableFuture<IDevice> handleAccelerationError(@Nullable final Project project,
            @NotNull final AvdInfo info, @NotNull AccelerationErrorCode error) {
        switch (error) {
        case ALREADY_INSTALLED:
            return null;
        case TOOLS_UPDATE_REQUIRED:
        case PLATFORM_TOOLS_UPDATE_ADVISED:
        case SYSTEM_IMAGE_UPDATE_ADVISED:
            // Do not block emulator from running if we need updates (run with degradated performance):
            return null;
        case NO_EMULATOR_INSTALLED:
            // report this error below
            break;
        default:
            Abi abi = Abi.getEnum(info.getAbiType());
            boolean isAvdIntel = abi == Abi.X86 || abi == Abi.X86_64;
            if (!isAvdIntel) {
                // Do not block Arm and Mips emulators from running without an accelerator:
                return null;
            }
            // report all other errors
            break;
        }
        String accelerator = SystemInfo.isLinux ? "KVM" : "Intel HAXM";
        int result = Messages.showOkCancelDialog(project,
                String.format("%1$s is required to run this AVD.\n%2$s\n\n%3$s\n", accelerator, error.getProblem(),
                        error.getSolutionMessage()),
                error.getSolution().getDescription(), AllIcons.General.WarningDialog);
        if (result != Messages.OK || error.getSolution() == AccelerationErrorSolution.SolutionCode.NONE) {
            return Futures.immediateFailedFuture(new RuntimeException("Could not start AVD"));
        }
        final SettableFuture<ListenableFuture<IDevice>> future = SettableFuture.create();
        Runnable retry = () -> future.set(startAvd(project, info));
        Runnable cancel = () -> future.setException(new RuntimeException("Retry after fixing problem by hand"));
        Runnable action = AccelerationErrorSolution.getActionForFix(error, project, retry, cancel);
        ApplicationManager.getApplication().invokeLater(action);
        return Futures.dereference(future);
    }

    /**
     * Run "emulator -accel-check" to check the status for emulator acceleration on this machine.
     * Return a {@link AccelerationErrorCode}.
     */
    public AccelerationErrorCode checkAcceleration() {
        if (!initIfNecessary()) {
            return AccelerationErrorCode.UNKNOWN_ERROR;
        }
        File emulatorBinary = getEmulatorBinary();
        if (!emulatorBinary.isFile()) {
            return AccelerationErrorCode.NO_EMULATOR_INSTALLED;
        }
        if (getMemorySize() < Storage.Unit.GiB.getNumberOfBytes()) {
            // TODO: The emulator -accel-check current does not check for the available memory, do it here instead:
            return AccelerationErrorCode.NOT_ENOUGH_MEMORY;
        }
        if (!hasQEMU2Installed()) {
            return AccelerationErrorCode.TOOLS_UPDATE_REQUIRED;
        }
        File checkBinary = getEmulatorCheckBinary();
        GeneralCommandLine commandLine = new GeneralCommandLine();
        if (checkBinary.isFile()) {
            commandLine.setExePath(checkBinary.getPath());
            commandLine.addParameter("accel");
        } else {
            commandLine.setExePath(emulatorBinary.getPath());
            commandLine.addParameter("-accel-check");
        }
        int exitValue;
        try {
            CapturingAnsiEscapesAwareProcessHandler process = new CapturingAnsiEscapesAwareProcessHandler(
                    commandLine);
            ProcessOutput output = process.runProcess();
            exitValue = output.getExitCode();
        } catch (ExecutionException e) {
            exitValue = AccelerationErrorCode.UNKNOWN_ERROR.getErrorCode();
        }
        if (exitValue != 0) {
            return AccelerationErrorCode.fromExitCode(exitValue);
        }
        if (!hasPlatformToolsForQEMU2Installed()) {
            return AccelerationErrorCode.PLATFORM_TOOLS_UPDATE_ADVISED;
        }
        if (!hasSystemImagesForQEMU2Installed()) {
            return AccelerationErrorCode.SYSTEM_IMAGE_UPDATE_ADVISED;
        }
        return AccelerationErrorCode.ALREADY_INSTALLED;
    }

    /**
     * Update the given AVD with the new settings or create one if no AVD is specified.
     * Returns the created AVD.
     */
    @Nullable
    public AvdInfo createOrUpdateAvd(@Nullable AvdInfo currentInfo, @NotNull String avdName, @NotNull Device device,
            @NotNull SystemImageDescription systemImageDescription, @NotNull ScreenOrientation orientation,
            boolean isCircular, @Nullable String sdCard, @Nullable File skinFolder,
            @NotNull Map<String, String> hardwareProperties, boolean createSnapshot) {
        if (!initIfNecessary()) {
            return null;
        }

        File avdFolder;
        try {
            if (currentInfo != null) {
                avdFolder = new File(currentInfo.getDataFolderPath());
            } else {
                avdFolder = AvdInfo.getDefaultAvdFolder(myAvdManager, avdName, myFileOp, true);
            }
        } catch (AndroidLocation.AndroidLocationException e) {
            IJ_LOG.error("Could not create AVD " + avdName, e);
            return null;
        }

        Dimension resolution = device.getScreenSize(orientation);
        assert resolution != null;
        String skinName = null;

        if (skinFolder == null && isCircular) {
            skinFolder = getRoundSkin(systemImageDescription);
        }
        if (FileUtil.filesEqual(skinFolder, AvdWizardUtils.NO_SKIN)) {
            skinFolder = null;
        }
        if (skinFolder == null) {
            skinName = String.format("%dx%d", Math.round(resolution.getWidth()),
                    Math.round(resolution.getHeight()));
        }
        if (orientation == ScreenOrientation.LANDSCAPE) {
            hardwareProperties.put(HardwareProperties.HW_INITIAL_ORIENTATION,
                    ScreenOrientation.LANDSCAPE.getShortDisplayValue().toLowerCase(Locale.ROOT));
        }
        if (currentInfo != null && !avdName.equals(currentInfo.getName())) {
            boolean success = myAvdManager.moveAvd(currentInfo, avdName, currentInfo.getDataFolderPath(), SDK_LOG);
            if (!success) {
                return null;
            }
        }

        return myAvdManager.createAvd(avdFolder, avdName, systemImageDescription.getSystemImage(), skinFolder,
                skinName, sdCard, hardwareProperties, device.getBootProps(), createSnapshot, false,
                currentInfo != null, SDK_LOG);
    }

    @Nullable
    private static File getRoundSkin(SystemImageDescription systemImageDescription) {
        File[] skins = systemImageDescription.getSkins();
        for (File skin : skins) {
            if (skin.getName().contains("Round")) {
                return skin;
            }
        }
        return null;
    }

    public static boolean doesSystemImageSupportQemu2(@Nullable SystemImageDescription description,
            @NotNull FileOp fileOp) {
        if (description == null) {
            return false;
        }
        ISystemImage systemImage = description.getSystemImage();
        if (systemImage == null) {
            return false;
        }
        File location = systemImage.getLocation();
        if (!fileOp.isDirectory(location)) {
            return false;
        }
        String[] files = fileOp.list(location, null);
        if (files != null) {
            for (String filename : files) {
                if (FileUtil.getNameWithoutExtension(filename).equals("kernel-ranchu")) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean avdExists(String candidate) {
        if (!initIfNecessary()) {
            return false;
        }
        return myAvdManager.getAvd(candidate, false) != null;
    }

    static boolean isAvdRepairable(@NotNull AvdInfo.AvdStatus avdStatus) {
        return avdStatus == AvdInfo.AvdStatus.ERROR_IMAGE_DIR || avdStatus == AvdInfo.AvdStatus.ERROR_DEVICE_CHANGED
                || avdStatus == AvdInfo.AvdStatus.ERROR_DEVICE_MISSING
                || avdStatus == AvdInfo.AvdStatus.ERROR_IMAGE_MISSING;
    }

    public static boolean isSystemImageDownloadProblem(@NotNull AvdInfo.AvdStatus status) {
        switch (status) {
        case ERROR_IMAGE_DIR:
        case ERROR_IMAGE_MISSING:
            return true;
        default:
            return false;
        }
    }

    public AvdInfo reloadAvd(@NotNull AvdInfo avdInfo) throws AndroidLocation.AndroidLocationException {
        return myAvdManager.reloadAvd(avdInfo, SDK_LOG);
    }

    @Nullable
    public static String getRequiredSystemImagePath(@NotNull AvdInfo avdInfo) {
        String imageSystemDir = avdInfo.getProperties().get(AvdManager.AVD_INI_IMAGES_1);
        if (imageSystemDir == null) {
            return null;
        }
        return StringUtil.trimEnd(imageSystemDir.replace(File.separatorChar, RepoPackage.PATH_SEPARATOR),
                RepoPackage.PATH_SEPARATOR);
    }

    public boolean updateDeviceChanged(@NotNull AvdInfo avdInfo) {
        if (initIfNecessary()) {
            try {
                return myAvdManager.updateDeviceChanged(avdInfo, SDK_LOG) != null;
            } catch (IOException e) {
                IJ_LOG.warn("Could not update AVD Device " + avdInfo.getName(), e);
            }
        }
        return false;
    }

    public boolean wipeUserData(@NotNull AvdInfo avdInfo) {
        if (initIfNecessary()) {
            File userdataImage = new File(avdInfo.getDataFolderPath(), "userdata-qemu.img");
            if (userdataImage.isFile()) {
                return userdataImage.delete();
            }
            return true;
        }
        return false;
    }

    public static String getAvdDisplayName(@NotNull AvdInfo avdInfo) {
        String displayName = avdInfo.getProperties().get(AvdManager.AVD_INI_DISPLAY_NAME);
        if (displayName == null) {
            displayName = avdInfo.getName().replaceAll("[_-]+", " ");
        }
        return displayName;
    }

    public String uniquifyDisplayName(String name) {
        int suffix = 1;
        String result = name;
        while (findAvdWithName(result)) {
            result = String.format("%1$s %2$d", name, ++suffix);
        }
        return result;
    }

    public boolean findAvdWithName(String name) {
        for (AvdInfo avd : getAvds(false)) {
            if (getAvdDisplayName(avd).equals(name)) {
                return true;
            }
        }
        return false;
    }

    public static long getMemorySize() {
        if (ourMemorySize < 0) {
            ourMemorySize = checkMemorySize();
        }
        return ourMemorySize;
    }

    private static long checkMemorySize() {
        OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
        // This is specific to JDKs derived from Oracle JDK (including OpenJDK and Apple JDK among others).
        // Other then this, there's no standard way of getting memory size
        // without adding 3rd party libraries or using native code.
        try {
            Class<?> oracleSpecificMXBean = Class.forName("com.sun.management.OperatingSystemMXBean");
            Method getPhysicalMemorySizeMethod = oracleSpecificMXBean.getMethod("getTotalPhysicalMemorySize");
            Object result = getPhysicalMemorySizeMethod.invoke(osMXBean);
            if (result instanceof Number) {
                return ((Number) result).longValue();
            }
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            // Unsupported JDK
        } catch (InvocationTargetException | IllegalAccessException e) {
            IJ_LOG.error(e); // Shouldn't happen (unsupported JDK?)
        }
        // Maximum memory allocatable to emulator - 32G. Only used if non-Oracle JRE.
        return 32L * Storage.Unit.GiB.getNumberOfBytes();
    }

    private static class SystemImageUpdateDependency {
        private final int myFeatureLevel;
        private final IdDisplay myTag;
        private final int myRequiredMajorRevision;

        public SystemImageUpdateDependency(int featureLevel, @NotNull IdDisplay tag, int requiredMajorRevision) {
            myFeatureLevel = featureLevel;
            myTag = tag;
            myRequiredMajorRevision = requiredMajorRevision;
        }

        public boolean updateRequired(@NotNull SystemImage image) {
            return updateRequired(image.getAbiType(), image.getAndroidVersion().getFeatureLevel(), image.getTag(),
                    image.getRevision());
        }

        public boolean updateRequired(@NotNull String abiType, int featureLevel, @NotNull IdDisplay tag,
                @NotNull Revision revision) {
            Abi abi = Abi.getEnum(abiType);
            boolean isAvdIntel = abi == Abi.X86 || abi == Abi.X86_64;

            return isAvdIntel && featureLevel == myFeatureLevel && myTag.equals(tag)
                    && revision.getMajor() < myRequiredMajorRevision;
        }
    }
}