Emulator launch options dialog.

This is displayed when clicking the Start button in the AVD manager.

The dialog allows to easily scale the emulator to match an arbitrary screen size
and to wipe the user data if needed.

The required monitor dpi is computed using java.awt.Toolkit, but (at least
on MacOS) it doesn't return the correct value, so it can be user supplied.
It's stored at least while the app is running and if possible in the settings
of the ADV/SDK Manager/Updater.

The wipe-data and scale flags are stored and reused while the app keeps
running as well.

Change-Id: Ia2f3ff5f4de285a3d505c6914d6b89cc663be284
This commit is contained in:
Xavier Ducrohet
2009-10-01 17:22:22 -07:00
parent ef1337876b
commit d13d440d43
6 changed files with 585 additions and 43 deletions

View File

@@ -110,7 +110,7 @@ public final class AvdManager {
/**
* Pattern to match pixel-sized skin "names", e.g. "320x480".
*/
public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("[0-9]{2,}x[0-9]{2,}"); //$NON-NLS-1$
public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$
private final static String USERDATA_IMG = "userdata.img"; //$NON-NLS-1$
private final static String CONFIG_INI = "config.ini"; //$NON-NLS-1$

View File

@@ -56,6 +56,7 @@ public class AvdManagerPage extends Composite implements ISdkListener {
mUpdaterData.getOsSdkRoot(),
mUpdaterData.getAvdManager(),
DisplayMode.MANAGER);
mAvdSelector.setSettingsController(mUpdaterData.getSettingsController());
}
@Override

View File

@@ -52,6 +52,12 @@ public interface ISettingsPage {
* Default: True.
*/
public static final String KEY_ASK_ADB_RESTART = "sdkman.ask.adb.restart"; //$NON-NLS-1$
/**
* Setting to set the density of the monitor.
* Type: Integer.
* Default: -1
*/
public static final String KEY_MONITOR_DENSITY = "sdkman.monitor.density"; //$NON-NLS-1$
/** Loads settings from the given {@link Properties} container and update the page UI. */
public abstract void loadSettings(Properties in_settings);

View File

@@ -46,7 +46,7 @@ public class SettingsController {
//--- Access to settings ------------
/**
* Returns the value of the ISettingsPage#KEY_FORCE_HTTP setting.
* Returns the value of the {@link ISettingsPage#KEY_FORCE_HTTP} setting.
* @see ISettingsPage#KEY_FORCE_HTTP
*/
public boolean getForceHttp() {
@@ -54,7 +54,7 @@ public class SettingsController {
}
/**
* Returns the value of the ISettingsPage#KEY_ASK_ADB_RESTART setting.
* Returns the value of the {@link ISettingsPage#KEY_ASK_ADB_RESTART} setting.
* @see ISettingsPage#KEY_ASK_ADB_RESTART
*/
public boolean getAskBeforeAdbRestart() {
@@ -66,7 +66,7 @@ public class SettingsController {
}
/**
* Returns the value of the ISettingsPage#KEY_SHOW_UPDATE_ONLY setting.
* Returns the value of the {@link ISettingsPage#KEY_SHOW_UPDATE_ONLY} setting.
* @see ISettingsPage#KEY_SHOW_UPDATE_ONLY
*/
public boolean getShowUpdateOnly() {
@@ -78,7 +78,7 @@ public class SettingsController {
}
/**
* Sets the value of the ISettingsPage#KEY_SHOW_UPDATE_ONLY setting.
* Sets the value of the {@link ISettingsPage#KEY_SHOW_UPDATE_ONLY} setting.
* @param enabled True if only compatible update items should be shown.
* @see ISettingsPage#KEY_SHOW_UPDATE_ONLY
*/
@@ -86,6 +86,32 @@ public class SettingsController {
setSetting(ISettingsPage.KEY_SHOW_UPDATE_ONLY, enabled);
}
/**
* Returns the value of the {@link ISettingsPage#KEY_MONITOR_DENSITY} setting
* @see ISettingsPage#KEY_MONITOR_DENSITY
*/
public int getMonitorDensity() {
String value = mProperties.getProperty(ISettingsPage.KEY_MONITOR_DENSITY, null);
if (value == null) {
return -1;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Sets the value of the {@link ISettingsPage#KEY_MONITOR_DENSITY} setting.
* @param density the density of the monitor
* @see ISettingsPage#KEY_MONITOR_DENSITY
*/
public void setMonitorDensity(int density) {
mProperties.setProperty(ISettingsPage.KEY_MONITOR_DENSITY, Integer.toString(density));
}
/**
* Internal helper to set a boolean setting.
*/

View File

@@ -26,6 +26,7 @@ import com.android.sdklib.internal.avd.AvdManager.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager.AvdInfo.AvdStatus;
import com.android.sdklib.internal.repository.ITask;
import com.android.sdklib.internal.repository.ITaskMonitor;
import com.android.sdkuilib.internal.repository.SettingsController;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import com.android.sdkuilib.internal.tasks.ProgressTask;
import com.android.sdkuilib.repository.UpdaterWindow;
@@ -95,6 +96,8 @@ public final class AvdSelector {
private Image mOkImage;
private Image mBrokenImage;
private SettingsController mController;
/**
* The display mode of the AVD Selector.
@@ -380,6 +383,14 @@ public final class AvdSelector {
DisplayMode displayMode) {
this(parent, osSdkPath, manager, new TargetBasedFilter(filter), displayMode);
}
/**
* Sets an optional SettingsController.
* @param controller the controller.
*/
public void setSettingsController(SettingsController controller) {
mController = controller;
}
/**
* Sets the table grid layout data.
*
@@ -898,6 +909,9 @@ public final class AvdSelector {
return;
}
AvdStartDialog dialog = new AvdStartDialog(mTable.getShell(), avdInfo, mOsSdkPath,
mController);
if (dialog.open() == Window.OK) {
String path = mOsSdkPath +
File.separator +
SdkConstants.OS_SDK_TOOLS_FOLDER +
@@ -910,6 +924,17 @@ public final class AvdSelector {
list.add(path);
list.add("-avd"); //$NON-NLS-1$
list.add(avdName);
if (dialog.getWipeData()) {
list.add("-wipe-data"); //$NON-NLS-1$
}
float scale = dialog.getScale();
if (scale != 0.f) {
// do the rounding ourselves. This is because %.1f will write .4899 as .4
scale = Math.round(scale * 100);
scale /= 100.f;
list.add("-scale"); //$NON-NLS-1$
list.add(String.format("%.2f", scale)); //$NON-NLS-1$
}
// convert the list into an array for the call to exec.
final String[] command = list.toArray(new String[list.size()]);
@@ -920,16 +945,19 @@ public final class AvdSelector {
new ITask() {
public void run(ITaskMonitor monitor) {
try {
monitor.setDescription("Starting emulator for AVD '%1$s'", avdName);
monitor.setDescription("Starting emulator for AVD '%1$s'",
avdName);
int n = 10;
monitor.setProgressMax(n);
Process process = Runtime.getRuntime().exec(command);
grabEmulatorOutput(process, monitor);
// This small wait prevents the dialog from closing too fast:
// When it works, the emulator returns immediately, even if no UI
// is shown yet. And when it fails (because the AVD is locked/running)
// if we don't have a wait we don't capture the error for some reason.
// When it works, the emulator returns immediately, even if
// no UI is shown yet. And when it fails (because the AVD is
// locked/running)
// if we don't have a wait we don't capture the error for
// some reason.
for (int i = 0; i < n; i++) {
try {
Thread.sleep(100);
@@ -939,11 +967,12 @@ public final class AvdSelector {
}
}
} catch (IOException e) {
monitor.setResult("Failed to start emulator: %1$s", e.getMessage());
monitor.setResult("Failed to start emulator: %1$s",
e.getMessage());
}
}
});
}
}
/**

View File

@@ -0,0 +1,480 @@
/*
* Copyright (C) 2009 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.sdkuilib.internal.widgets;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.avd.AvdManager.AvdInfo;
import com.android.sdkuilib.internal.repository.SettingsController;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import java.awt.Toolkit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Dialog dealing with emulator launch options. The following options are supported:
* <ul>
* <li>-wipe-data</li>
* <li>-scale</li>
* </ul>
*
* Values are stored (in the class as static field) to be reused while the app is still running.
* The Monitor dpi is stored in the settings if availabe.
*/
final class AvdStartDialog extends Dialog {
// static field to reuse values during the same session.
private static boolean sWipeData = false;
private static int sMonitorDpi = 72; // used if there's no setting controller.
private static final Map<String, String> sSkinScaling = new HashMap<String, String>();
private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?");
private final AvdInfo mAvd;
private final String mSdkLocation;
private final SettingsController mSettingsController;
private Text mScreenSize;
private Text mMonitorDpi;
private Button mScaleButton;
private float mScale = 0.f;
private boolean mWipeData = false;
private int mDensity = 160; // medium density
private int mSize1 = -1;
private int mSize2 = -1;
private String mSkinDisplay;
private boolean mEnableScaling = true;
protected AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation,
SettingsController settingsController) {
super(parentShell);
mAvd = avd;
mSdkLocation = sdkLocation;
mSettingsController = settingsController;
if (mAvd == null) {
throw new IllegalArgumentException("avd cannot be null");
}
if (mSdkLocation == null) {
throw new IllegalArgumentException("sdkLocation cannot be null");
}
computeSkinData();
}
public boolean getWipeData() {
return mWipeData;
}
/**
* Returns the scaling factor, or 0.f if none are set.
*/
public float getScale() {
return mScale;
}
@Override
protected Control createDialogArea(Composite parent) {
GridData gd;
// create a composite with standard margins and spacing
Composite composite = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(2, false);
layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
layout.horizontalSpacing = convertHorizontalDLUsToPixels(
IDialogConstants.HORIZONTAL_SPACING);
composite.setLayout(layout);
composite.setLayoutData(new GridData(GridData.FILL_BOTH));
Label l = new Label(composite, SWT.NONE);
l.setText("Skin:");
l = new Label(composite, SWT.NONE);
l.setText(mSkinDisplay);
l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
l = new Label(composite, SWT.NONE);
l.setText("Density:");
l = new Label(composite, SWT.NONE);
l.setText(getDensityText());
l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mScaleButton = new Button(composite, SWT.CHECK);
mScaleButton.setText("Scale display to real size");
mScaleButton.setEnabled(mEnableScaling);
boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null;
mScaleButton.setSelection(defaultState);
mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
gd.horizontalSpan = 2;
final Group scaleGroup = new Group(composite, SWT.NONE);
scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
gd.horizontalIndent = 30;
gd.horizontalSpan = 2;
scaleGroup.setLayout(new GridLayout(2, false));
l = new Label(scaleGroup, SWT.NONE);
l.setText("Screen Size (in):");
mScreenSize = new Text(scaleGroup, SWT.BORDER);
mScreenSize.setText(getScreenSize());
mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mScreenSize.addVerifyListener(new VerifyListener() {
public void verifyText(VerifyEvent event) {
// combine the current content and the new text
String text = mScreenSize.getText();
text = text.substring(0, event.start) + event.text + text.substring(event.end);
// now make sure it's a match for the regex
event.doit = sScreenSizePattern.matcher(text).matches();
}
});
mScreenSize.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent event) {
onScaleChange();
}
});
l = new Label(scaleGroup, SWT.NONE);
l.setText("Monitor dpi:");
mMonitorDpi = new Text(scaleGroup, SWT.BORDER);
mMonitorDpi.setText(Integer.toString(getMonitorDpi()));
mMonitorDpi.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mMonitorDpi.addVerifyListener(new VerifyListener() {
public void verifyText(VerifyEvent event) {
// check for digit only.
for (int i = 0 ; i < event.text.length(); i++) {
char letter = event.text.charAt(i);
if (letter < '0' || letter > '9') {
event.doit = false;
return;
}
}
}
});
mMonitorDpi.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent event) {
onScaleChange();
}
});
scaleGroup.setEnabled(defaultState);
mScaleButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
boolean enabled = mScaleButton.getSelection();
scaleGroup.setEnabled(enabled);
if (enabled) {
onScaleChange();
} else {
mScale = 0.f;
}
}
});
final Button wipeButton = new Button(composite, SWT.CHECK);
wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
gd.horizontalSpan = 2;
wipeButton.setText("Wipe user data");
wipeButton.setSelection(mWipeData = sWipeData);
wipeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent arg0) {
mWipeData = wipeButton.getSelection();
}
});
l = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
gd.horizontalSpan = 2;
applyDialogFont(composite);
// if the scaling is enabled by default, we must initialize the value of mScale
if (defaultState) {
onScaleChange();
}
return composite;
}
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
newShell.setText("Launch Options");
}
@Override
protected Button createButton(Composite parent, int id, String label, boolean defaultButton) {
if (id == IDialogConstants.OK_ID) {
label = "Launch";
}
return super.createButton(parent, id, label, defaultButton);
}
@Override
protected void okPressed() {
// override ok to store some info
// first the monitor dpi
String dpi = mMonitorDpi.getText();
if (dpi.length() > 0) {
sMonitorDpi = Integer.parseInt(dpi);
// if there is a setting controller, save it
if (mSettingsController != null) {
mSettingsController.setMonitorDensity(sMonitorDpi);
mSettingsController.saveSettings();
}
}
// now the scale factor
String key = mAvd.getName();
sSkinScaling.remove(key);
if (mScaleButton.getSelection()) {
String size = mScreenSize.getText();
if (size.length() > 0) {
sSkinScaling.put(key, size);
}
}
// and then the wipe-data checkbox
sWipeData = mWipeData;
// finally continue with the ok action
super.okPressed();
}
private void computeSkinData() {
Map<String, String> prop = mAvd.getProperties();
String dpi = prop.get("hw.lcd.density");
if (dpi != null && dpi.length() > 0) {
mDensity = Integer.parseInt(dpi);
}
findSkinResolution();
}
private void onScaleChange() {
String sizeStr = mScreenSize.getText();
if (sizeStr.length() == 0) {
mScale = 0.f;
return;
}
String dpiStr = mMonitorDpi.getText();
if (dpiStr.length() == 0) {
mScale = 0.f;
return;
}
int dpi = Integer.parseInt(dpiStr);
float size = Float.parseFloat(sizeStr);
/*
* We are trying to emulate the following device:
* resolution: 'mSize1'x'mSize2'
* density: 'mDensity'
* screen diagonal: 'size'
* ontop a monitor running at 'dpi'
*/
// We start by computing the screen diagonal in pixels, if the density was really mDensity
float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2);
// Now we would convert this in actual inches:
// diagonalIn = diagonal / mDensity
// the scale factor is a mix of adapting to the new density and to the new size.
// (size/diagonalIn) * (dpi/mDensity)
// this can be simplified to:
mScale = (size * dpi) / diagonalPx;
}
/**
* Returns the monitor dpi to start with.
* This can be coming from the settings, the session-based storage, or the from whatever Java
* can tell us.
*/
private int getMonitorDpi() {
if (mSettingsController != null) {
sMonitorDpi = mSettingsController.getMonitorDensity();
}
if (sMonitorDpi == -1) { // first time? try to get a value
sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution();
}
return sMonitorDpi;
}
/**
* Returns the screen size to start with.
* <p/>If an emulator with the same skin was already launched, scaled, the size used is reused.
* <p/>Otherwise the default is returned (3)
*/
private String getScreenSize() {
String size = sSkinScaling.get(mAvd.getName());
if (size != null) {
return size;
}
return "3";
}
/**
* Returns a display string for the density.
*/
private String getDensityText() {
switch (mDensity) {
case 120:
return "Low (120)";
case 160:
return "Medium (160)";
case 240:
return "High (240)";
}
return Integer.toString(mDensity);
}
/**
* Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}.
*/
private void findSkinResolution() {
Map<String, String> prop = mAvd.getProperties();
String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME);
Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName);
if (m.matches()) {
mSize1 = Integer.parseInt(m.group(1));
mSize2 = Integer.parseInt(m.group(2));
mSkinDisplay = skinName;
mEnableScaling = true;
} else {
// The resolution is inside the layout file of the skin.
mEnableScaling = false; // default to false for now.
// path to the skin layout file.
File skinFolder = new File(mSdkLocation, prop.get(AvdManager.AVD_INI_SKIN_PATH));
if (skinFolder.isDirectory()) {
File layoutFile = new File(skinFolder, "layout");
if (layoutFile.isFile()) {
if (parseLayoutFile(layoutFile)) {
mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2);
mEnableScaling = true;
} else {
mSkinDisplay = skinName;
}
}
}
}
}
/**
* Parses a layout file.
* <p/>
* the format is relatively easy. It's a collection of items defined as
* &lg;name&gt; {
* &lg;content&gt;
* }
*
* content is either 1+ items or 1+ properties
* properties are defined as
* &lg;name&gt;&lg;whitespace&gt;&lg;value&gt;
*
* We're going to look for an item called display, with 2 properties height and width.
* This is very basic parser.
*
* @param layoutFile the file to parse
* @return true if both sizes where found.
*/
private boolean parseLayoutFile(File layoutFile) {
try {
BufferedReader input = new BufferedReader(new FileReader(layoutFile));
String line;
while ((line = input.readLine()) != null) {
// trim to remove whitespace
line = line.trim();
int len = line.length();
if (len == 0) continue;
// check if this is a new item
if (line.charAt(len-1) == '{') {
// this is the start of a node
String[] tokens = line.split(" ");
if ("display".equals(tokens[0])) {
// this is the one we're looking for!
while ((mSize1 == -1 || mSize2 == -1) &&
(line = input.readLine()) != null) {
// trim to remove whitespace
line = line.trim();
len = line.length();
if (len == 0) continue;
if ("}".equals(line)) { // looks like we're done with the item.
break;
}
tokens = line.split(" ");
if (tokens.length >= 2) {
// there can be multiple space between the name and value
// in which case we'll get an extra empty token in the middle.
if ("width".equals(tokens[0])) {
mSize1 = Integer.parseInt(tokens[tokens.length-1]);
} else if ("height".equals(tokens[0])) {
mSize2 = Integer.parseInt(tokens[tokens.length-1]);
}
}
}
return mSize1 != -1 && mSize2 != -1;
}
}
}
// if it reaches here, display was not found.
// false is returned below.
} catch (IOException e) {
// ignore.
}
return false;
}
}