blob: efe416cec0a4fcd36e5f18e8b11982406181fb68 [file] [log] [blame]
/*
* 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.devices.Device;
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.local.LocalSdk;
import com.android.tools.idea.run.ExternalToolRunner;
import com.android.tools.idea.sdk.LogWrapper;
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.execution.process.ProcessHandler;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.util.ProgressWindow;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.containers.WeakHashMap;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
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;
import static com.android.tools.idea.avdmanager.AvdWizardConstants.NO_SKIN;
/**
* 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);
public static final String AVD_INI_HW_LCD_DENSITY = "hw.lcd.density";
public static final String AVD_INI_DISPLAY_NAME = "avd.ini.displayname";
private static final AvdManagerConnection NULL_CONNECTION = new AvdManagerConnection(null);
private AvdManager ourAvdManager;
private Map<File, SkinLayoutDefinition> ourSkinLayoutDefinitions = Maps.newHashMap();
private File ourEmulatorBinary;
private static Map<LocalSdk, AvdManagerConnection> ourCache = new WeakHashMap<LocalSdk, AvdManagerConnection>();
@Nullable private final LocalSdk myLocalSdk;
@NotNull
public static AvdManagerConnection getDefaultAvdManagerConnection() {
AndroidSdkData androidSdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
LocalSdk localSdk = null;
if (androidSdkData != null) {
localSdk = androidSdkData.getLocalSdk();
}
if (localSdk == null) {
return NULL_CONNECTION;
}
else {
return getAvdManagerConnection(localSdk);
}
}
@NotNull
public synchronized static AvdManagerConnection getAvdManagerConnection(@NotNull LocalSdk localSdk) {
if (!ourCache.containsKey(localSdk)) {
ourCache.put(localSdk, new AvdManagerConnection(localSdk));
}
return ourCache.get(localSdk);
}
private AvdManagerConnection(@Nullable LocalSdk localSdk) {
myLocalSdk = localSdk;
}
/**
* Setup our static instances if required. If the instance already exists, then this is a no-op.
*/
private boolean initIfNecessary() {
if (ourAvdManager == null) {
if (myLocalSdk == null) {
IJ_LOG.error("No Android SDK Found");
return false;
}
try {
ourAvdManager = AvdManager.getInstance(myLocalSdk, 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 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 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 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 void deleteAvd(@NotNull AvdInfo info) {
if (!initIfNecessary()) {
return;
}
ourAvdManager.deleteAvd(info, SDK_LOG);
}
public boolean isAvdRunning(@NotNull AvdInfo info) {
return ourAvdManager.isAvdRunning(info);
}
public void stopAvd(@NotNull final AvdInfo info) {
ourAvdManager.stopAvd(info);
}
/**
* Launch the given AVD in the emulator.
*/
public void startAvd(@Nullable final Project project, @NotNull final AvdInfo info) {
if (!initIfNecessary()) {
return;
}
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 (ourAvdManager.isAvdRunning(info)) {
String baseFolder;
try {
baseFolder = ourAvdManager.getBaseAvdFolder();
}
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;
}
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, project);
p.setIndeterminate(false);
p.setDelayInMillis(0);
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
GeneralCommandLine commandLine = new GeneralCommandLine();
commandLine.setExePath(ourEmulatorBinary.getPath());
// Don't explicitly set auto since that seems to be the default behavior, but when set
// can cause the emulator to fail to launch with this error message:
// "could not get monitor DPI resolution from system. please use -dpi-monitor to specify one"
// (this happens on OSX where we don't have a reliable, Retina-correct way to get the dpi)
if (scaleFactor != null && !"auto".equals(scaleFactor)) {
commandLine.addParameters("-scale", scaleFactor);
}
if (netDelay != null) {
commandLine.addParameters("-netdelay", netDelay);
}
if (netSpeed != null) {
commandLine.addParameters("-netspeed", netSpeed);
}
commandLine.addParameters("-avd", avdName);
EmulatorRunner runner = new EmulatorRunner(project, "AVD: " + avdName, commandLine, info);
ProcessHandler processHandler;
try {
processHandler = runner.start();
}
catch (ExecutionException e) {
IJ_LOG.error("Error launching emulator", e);
return;
}
ExternalToolRunner.ProcessOutputCollector collector = new ExternalToolRunner.ProcessOutputCollector();
processHandler.addProcessListener(collector);
// 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 manager
// window.
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();
}
processHandler.removeProcessListener(collector);
final String message = collector.getText();
if (message.toLowerCase().contains("error") || processHandler.isProcessTerminated() && !message.trim().isEmpty()) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
Messages.showErrorDialog(project, "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 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(ourAvdManager, avdName, 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, NO_SKIN)) {
skinFolder = null;
hardwareProperties.remove(AvdManager.AVD_INI_SKIN_PATH);
}
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());
}
if (currentInfo != null && !avdName.equals(currentInfo.getName())) {
boolean success = ourAvdManager.moveAvd(currentInfo, avdName, currentInfo.getDataFolderPath(), SDK_LOG);
if (!success) {
return null;
}
}
return ourAvdManager.createAvd(avdFolder,
avdName,
systemImageDescription.getTarget(),
systemImageDescription.getTag(),
systemImageDescription.getAbiType(),
skinFolder,
skinName,
sdCard,
hardwareProperties,
device.getBootProps(),
createSnapshot,
false, // Remove Previous
currentInfo != null, // edit existing
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 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
|| avdStatus == AvdInfo.AvdStatus.ERROR_IMAGE_MISSING;
}
public 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 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 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(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 : getDefaultAvdManagerConnection().getAvds(false)) {
if (getAvdDisplayName(avd).equals(name)) {
return true;
}
}
return false;
}
}