/* * 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.prefs.AndroidLocation; import com.android.resources.Density; import com.android.resources.ScreenOrientation; import com.android.sdklib.ISystemImage; import com.android.sdklib.devices.Device; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; import com.android.sdklib.repository.local.LocalSdk; import com.android.utils.ILogger; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.util.ProgressWindow; import com.intellij.openapi.util.io.FileUtil; import org.jetbrains.android.sdk.AndroidSdkData; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.android.util.ExecutionStatus; import org.jetbrains.android.util.StringBuildingOutputProcessor; import org.jetbrains.android.util.WaitingStrategies; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; /** * A wrapper class for communicating with {@link com.android.sdklib.internal.avd.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) { @Override public void error(Throwable t, String errorFormat, Object... args) { IJ_LOG.error(String.format(errorFormat, args), t); } }; private static final String AVD_INI_HW_LCD_DENSITY = "hw.lcd.density"; private static AvdManager ourAvdManager; private static Map<File, SkinLayoutDefinition> ourSkinLayoutDefinitions = Maps.newHashMap(); private static File ourEmulatorBinary; /** * Setup our static instances if required. If the instance already exists, then this is a no-op. */ private static boolean initIfNecessary() { if (ourAvdManager == null) { AndroidSdkData androidSdkData = AndroidSdkUtils.tryToChooseAndroidSdk(); if (androidSdkData == null) { IJ_LOG.error("No Android SDK Found"); return false; } LocalSdk localSdk = androidSdkData.getLocalSdk(); try { ourAvdManager = AvdManager.getInstance(localSdk, SDK_LOG); } catch (AndroidLocation.AndroidLocationException e) { IJ_LOG.error("Could not instantiate AVD Manager from SDK", e); return false; } ourEmulatorBinary = new File(ourAvdManager.getLocalSdk().getLocation(), FileUtil.join(SdkConstants.OS_SDK_TOOLS_FOLDER, SdkConstants.FN_EMULATOR)); if (!ourEmulatorBinary.isFile()) { IJ_LOG.error("No emulator binary found!"); return false; } } return true; } /** * @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 static List<AvdInfo> getAvds(boolean forceRefresh) { if (!initIfNecessary()) { return ImmutableList.of(); } if (forceRefresh) { try { ourAvdManager.reloadAvds(SDK_LOG); } catch (AndroidLocation.AndroidLocationException e) { IJ_LOG.error("Could not find Android SDK!", e); } } ArrayList<AvdInfo> avdInfos = Lists.newArrayList(ourAvdManager.getAllAvds()); boolean needsRefresh = false; for (AvdInfo info : avdInfos) { if (info.getStatus() == AvdInfo.AvdStatus.ERROR_IMAGE_DIR) { updateAvdImageFolder(info); needsRefresh = true; } else if (info.getStatus() == AvdInfo.AvdStatus.ERROR_DEVICE_CHANGED) { updateDeviceChanged(info); needsRefresh = true; } } if (needsRefresh) { return getAvds(true); } else { return avdInfos; } } /** * @return a Dimension object representing the screen size of the given AVD in pixels or null if * the AVD does not define a resolution. */ @Nullable public static Dimension getAvdResolution(@NotNull AvdInfo info) { if (!initIfNecessary()) { return null; } Map<String, String> properties = info.getProperties(); String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); if (skin != null) { Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin); if (m.matches()) { int size1 = Integer.parseInt(m.group(1)); int size2 = Integer.parseInt(m.group(2)); return new Dimension(size1, size2); } } skin = properties.get(AvdManager.AVD_INI_SKIN_PATH); if (skin != null) { File skinPath = new File(skin); File skinDir; if (skinPath.isAbsolute()) { skinDir = skinPath; } else { skinDir = new File(ourAvdManager.getLocalSdk().getLocation(), skin); } if (skinDir.isDirectory()) { File layoutFile = new File(skinDir, "layout"); if (layoutFile.isFile()) { return getResolutionFromLayoutFile(layoutFile); } } } return null; } /** * Read the resolution from a layout definition file. See {@link SkinLayoutDefinition} for details on the format * of that file. */ @Nullable protected static Dimension getResolutionFromLayoutFile(@NotNull File layoutFile) { if (!ourSkinLayoutDefinitions.containsKey(layoutFile)) { ourSkinLayoutDefinitions.put(layoutFile, SkinLayoutDefinition.parseFile(layoutFile)); } SkinLayoutDefinition layoutDefinition = ourSkinLayoutDefinitions.get(layoutFile); if (layoutDefinition != null) { String heightString = layoutDefinition.get("parts.device.display.height"); String widthString = layoutDefinition.get("parts.device.display.width"); if (widthString == null || heightString == null) { return null; } int height = Integer.parseInt(heightString); int width = Integer.parseInt(widthString); return new Dimension(width, height); } return null; } /** * @return A string representing the AVD's screen density. One of ["ldpi", "mdpi", "hdpi", "xhdpi", "xxhdpi"] */ @Nullable public static Density getAvdDensity(@NotNull AvdInfo info) { Map<String, String> properties = info.getProperties(); String densityString = properties.get(AVD_INI_HW_LCD_DENSITY); if (densityString != null) { int density = Integer.parseInt(densityString); Density[] knownDensities = Density.values(); // Densities are declared high to low int i = 0; while (density < knownDensities[i].getDpiValue()) { i++; } if (i < knownDensities.length) { return knownDensities[i]; } else { return null; } } else { return null; } } /** * Delete the given AVD if it exists. */ public static void deleteAvd(@NotNull AvdInfo info) { if (!initIfNecessary()) { return; } ourAvdManager.deleteAvd(info, SDK_LOG); } /** * Launch the given AVD in the emulator. */ public static void startAvd(@NotNull final AvdInfo info) { if (!initIfNecessary()) { return; } final String avdName = info.getName(); if (info.isRunning()) { return; } Map<String, String> properties = info.getProperties(); final String scaleFactor = properties.get(AvdWizardConstants.AVD_INI_SCALE_FACTOR); final String netDelay = properties.get(AvdWizardConstants.AVD_INI_NETWORK_LATENCY); final String netSpeed = properties.get(AvdWizardConstants.AVD_INI_NETWORK_SPEED); final ProgressWindow p = new ProgressWindow(false, true, null); p.setIndeterminate(false); p.setDelayInMillis(0); ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { @Override public void run() { GeneralCommandLine commandLine = new GeneralCommandLine(); commandLine.setExePath(ourEmulatorBinary.getPath()); if (scaleFactor != null) { commandLine.addParameters("-scale", scaleFactor); } if (netDelay != null) { commandLine.addParameters("-netdelay", netDelay); } if (netSpeed != null) { commandLine.addParameters("-netspeed", netSpeed); } commandLine.addParameters("-avd", avdName); final StringBuildingOutputProcessor processor = new StringBuildingOutputProcessor(); try { if (AndroidUtils.executeCommand(commandLine, processor, WaitingStrategies.WaitForTime.getInstance(1000)) == ExecutionStatus.TIMEOUT) { // It takes about 2 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 manager // window. try { p.start(); p.setText("Starting AVD..."); for (double d = 0; d < 1; d += 1.0 / 20) { p.setFraction(d); //noinspection BusyWait Thread.sleep(100); } } catch (InterruptedException ignore) { } finally { p.stop(); } return; } } catch (ExecutionException e) { IJ_LOG.error(e); return; } final String message = processor.getMessage(); if (message.toLowerCase().contains("error")) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { IJ_LOG.error("Cannot launch AVD in emulator.\nOutput:\n" + message, avdName); } }); } } }); } /** * Update the given AVD with the new settings or create one if no AVD is specified. * Returns the created AVD. */ @Nullable public static AvdInfo createOrUpdateAvd(@Nullable AvdInfo currentInfo, @NotNull String avdName, @NotNull Device device, @NotNull AvdWizardConstants.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 { avdFolder = AvdInfo.getDefaultAvdFolder(ourAvdManager, avdName); } catch (AndroidLocation.AndroidLocationException e) { IJ_LOG.error("Could not create AVD " + avdName, e); return null; } ISystemImage image = systemImageDescription.systemImage; // TODO: Fix this so that the screen appears in the proper orientation Dimension resolution = device.getScreenSize(device.getDefaultState().getOrientation()); //device.getScreenSize(orientation); assert resolution != null; String skinName = null; if (skinFolder == null && isCircular) { skinFolder = getRoundSkin(systemImageDescription); } if (skinFolder == null) { skinName = String.format("%dx%d", Math.round(resolution.getWidth()), Math.round(resolution.getHeight())); } return ourAvdManager.createAvd(avdFolder, avdName, systemImageDescription.target, image.getTag(), image.getAbiType(), skinFolder, skinName, sdCard, hardwareProperties, device.getBootProps(), createSnapshot, false, // Remove Previous currentInfo != null, // edit existing SDK_LOG); } @Nullable private static File getRoundSkin(AvdWizardConstants.SystemImageDescription systemImageDescription) { File[] skins = systemImageDescription.systemImage.getSkins(); for (File skin : skins) { if (skin.getName().contains("Round")) { return skin; } } return null; } public static boolean avdExists(String candidate) { if (!initIfNecessary()) { return false; } return ourAvdManager.getAvd(candidate, false) != null; } static boolean isAvdRepairable(AvdInfo.AvdStatus avdStatus) { return avdStatus == AvdInfo.AvdStatus.ERROR_IMAGE_DIR || avdStatus == AvdInfo.AvdStatus.ERROR_DEVICE_CHANGED || avdStatus == AvdInfo.AvdStatus.ERROR_DEVICE_MISSING; } public static boolean updateAvdImageFolder(@NotNull AvdInfo avdInfo) { if (initIfNecessary()) { try { ourAvdManager.updateAvd(avdInfo, SDK_LOG); return true; } catch (IOException e) { IJ_LOG.error("Could not update AVD " + avdInfo.getName(), e); } } return false; } public static boolean updateDeviceChanged(@NotNull AvdInfo avdInfo) { if (initIfNecessary()) { try { ourAvdManager.updateDeviceChanged(avdInfo, SDK_LOG); return true; } catch (IOException e) { IJ_LOG.error("Could not update AVD Device " + avdInfo.getName(), e); } } return false; } public static 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; } }