auto import from //depot/cupcake/@136594

This commit is contained in:
The Android Open Source Project
2009-03-05 14:34:30 -08:00
parent 52d4c30ca5
commit edd86fdaa9
24 changed files with 596 additions and 148 deletions

View File

@@ -174,14 +174,16 @@ public class SoftKeyboard extends InputMethodService
// We now look for a few special variations of text that will
// modify our behavior.
int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION;
if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD) {
if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||
variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) {
// Do not display predictions / what the user is typing
// when they are entering a password.
mPredictionOn = false;
}
if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|| variation == EditorInfo.TYPE_TEXT_VARIATION_URI) {
|| variation == EditorInfo.TYPE_TEXT_VARIATION_URI
|| variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) {
// Our predictions are not useful for e-mail addresses
// or URIs.
mPredictionOn = false;
@@ -207,6 +209,7 @@ public class SoftKeyboard extends InputMethodService
// For all unknown input types, default to the alphabetic
// keyboard with no special features.
mCurKeyboard = mQwertyKeyboard;
updateShiftKeyState(attribute);
}
// Update the label on the enter key, depending on what the application
@@ -428,7 +431,11 @@ public class SoftKeyboard extends InputMethodService
private void updateShiftKeyState(EditorInfo attr) {
if (attr != null
&& mInputView != null && mQwertyKeyboard == mInputView.getKeyboard()) {
int caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType);
int caps = 0;
EditorInfo ei = getCurrentInputEditorInfo();
if (ei != null && ei.inputType != EditorInfo.TYPE_NULL) {
caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType);
}
mInputView.setShifted(mCapsLock || caps != 0);
}
}
@@ -553,7 +560,7 @@ public class SoftKeyboard extends InputMethodService
final int length = mComposing.length();
if (length > 1) {
mComposing.delete(length - 1, length);
getCurrentInputConnection().setComposingText(mComposing, mComposing.length());
getCurrentInputConnection().setComposingText(mComposing, 1);
updateCandidates();
} else if (length > 0) {
mComposing.setLength(0);
@@ -594,7 +601,7 @@ public class SoftKeyboard extends InputMethodService
}
if (isAlphabet(primaryCode) && mPredictionOn) {
mComposing.append((char) primaryCode);
getCurrentInputConnection().setComposingText(mComposing, mComposing.length());
getCurrentInputConnection().setComposingText(mComposing, 1);
updateShiftKeyState(getCurrentInputEditorInfo());
updateCandidates();
} else {
@@ -605,7 +612,7 @@ public class SoftKeyboard extends InputMethodService
private void handleClose() {
commitTyped(getCurrentInputConnection());
dismissSoftInput(0);
requestHideSelf(0);
mInputView.closing();
}

View File

@@ -40,11 +40,12 @@ public class Application {
}
}
public static void main(String... args) {
public static void main(final String... args) {
initUserInterface();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
MainFrame frame = new MainFrame();
String arg = args.length > 0 ? args[0] : null;
MainFrame frame = new MainFrame(arg);
frame.setDefaultCloseOperation(MainFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);

View File

@@ -651,6 +651,7 @@ class ImageEditorPanel extends JPanel {
private int lastPositionX;
private int lastPositionY;
private int currentButton;
private boolean showCursor;
private JLabel helpLabel;
@@ -687,16 +688,20 @@ class ImageEditorPanel extends JPanel {
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
paint(event.getX(), event.getY(), event.isShiftDown() ? MouseEvent.BUTTON3 :
event.getButton());
// Store the button here instead of retrieving it again in MouseDragged
// below, because on linux, calling MouseEvent.getButton() for the drag
// event returns 0, which appears to be technically correct (no button
// changed state).
currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton();
paint(event.getX(), event.getY(), currentButton);
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent event) {
if (!checkLockedRegion(event.getX(), event.getY())) {
paint(event.getX(), event.getY(), event.isShiftDown() ? MouseEvent.BUTTON3 :
event.getButton());
// use the stored button, see note above
paint(event.getX(), event.getY(), currentButton);
}
}

View File

@@ -40,14 +40,24 @@ public class MainFrame extends JFrame {
private JMenuItem saveMenuItem;
private ImageEditorPanel imageEditor;
public MainFrame() throws HeadlessException {
public MainFrame(String path) throws HeadlessException {
super("Draw 9-patch");
buildActions();
buildMenuBar();
buildContent();
if (path == null) {
showOpenFilePanel();
} else {
try {
File file = new File(path);
BufferedImage img = GraphicsUtilities.loadCompatibleImage(file.toURI().toURL());
showImageEditor(img, file.getAbsolutePath());
} catch (Exception ex) {
showOpenFilePanel();
}
}
// pack();
setSize(1024, 600);

View File

@@ -346,7 +346,7 @@ public class ApkBuilder extends BaseBuilder {
}
// also check the final file(s)!
String finalPackageName = getFileName(project, null /*config*/);
String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/);
if (mBuildFinalPackage == false) {
tmp = outputFolder.findMember(finalPackageName);
if (tmp == null || (tmp instanceof IFile &&
@@ -359,7 +359,7 @@ public class ApkBuilder extends BaseBuilder {
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
String filename = getFileName(project, entry.getKey());
String filename = ProjectHelper.getApkFilename(project, entry.getKey());
tmp = outputFolder.findMember(filename);
if (tmp == null || (tmp instanceof IFile &&
@@ -409,7 +409,7 @@ public class ApkBuilder extends BaseBuilder {
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
String packageFilepath = osBinPath + File.separator +
getFileName(project, entry.getKey());
ProjectHelper.getApkFilename(project, entry.getKey());
finalPackage = new File(packageFilepath);
finalPackage.delete();
@@ -532,7 +532,7 @@ public class ApkBuilder extends BaseBuilder {
// make the filename for the apk to generate
String apkOsFilePath = osBinPath + File.separator +
getFileName(project, entry.getKey());
ProjectHelper.getApkFilename(project, entry.getKey());
if (finalPackage(resPath, classesDexPath, apkOsFilePath, javaProject,
referencedJavaProjects) == false) {
return referencedProjects;
@@ -1117,19 +1117,6 @@ public class ApkBuilder extends BaseBuilder {
return list.toArray(new IJavaProject[list.size()]);
}
/**
* Returns the apk filename for the given project
* @param project The project.
* @param config An optional config name. Can be null.
*/
private static String getFileName(IProject project, String config) {
if (config != null) {
return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$
}
return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
}
/**
* Checks a {@link IFile} to make sure it should be packaged as standard resources.
* @param file the IFile representing the file.

View File

@@ -665,4 +665,17 @@ public final class ProjectHelper {
return false;
}
/**
* Returns the apk filename for the given project
* @param project The project.
* @param config An optional config name. Can be null.
*/
public static String getApkFilename(IProject project, String config) {
if (config != null) {
return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$
}
return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
}
}

View File

@@ -18,14 +18,20 @@ package com.android.ide.eclipse.adt.project.export;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import com.android.jarutils.KeystoreHelper;
import com.android.jarutils.SignedJarBuilder;
import com.android.jarutils.DebugKeyProvider.IKeyGenOutput;
import com.android.jarutils.DebugKeyProvider.KeytoolException;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
@@ -35,12 +41,14 @@ import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IExportWizard;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
@@ -49,6 +57,9 @@ import java.security.KeyStore.PrivateKeyEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* Export wizard to export an apk signed with a release key/certificate.
@@ -66,6 +77,11 @@ public final class ExportWizard extends Wizard implements IExportWizard {
static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$
static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$
static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$
static final String PROPERTY_FILENAME = "baseFilename"; //$NON-NLS-1$
static final int APK_FILE_SOURCE = 0;
static final int APK_FILE_DEST = 1;
static final int APK_COUNT = 2;
/**
* Base page class for the ExportWizard page. This class add the {@link #onShow()} callback.
@@ -131,7 +147,7 @@ public final class ExportWizard extends Wizard implements IExportWizard {
* Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a
* {@link Throwable} object.
*/
protected final void onException(Throwable t) {
protected void onException(Throwable t) {
String message = getExceptionMessage(t);
setErrorMessage(message);
@@ -155,9 +171,7 @@ public final class ExportWizard extends Wizard implements IExportWizard {
private PrivateKey mPrivateKey;
private X509Certificate mCertificate;
private String mDestinationPath;
private String mApkFilePath;
private String mApkFileName;
private File mDestinationParentFolder;
private ExportWizardPage mKeystoreSelectionPage;
private ExportWizardPage mKeyCreationPage;
@@ -168,6 +182,8 @@ public final class ExportWizard extends Wizard implements IExportWizard {
private List<String> mExistingAliases;
private Map<String, String[]> mApkMap;
public ExportWizard() {
setHelpAvailable(false); // TODO have help
setWindowTitle("Export Android Application");
@@ -186,24 +202,46 @@ public final class ExportWizard extends Wizard implements IExportWizard {
@Override
public boolean performFinish() {
// first we make sure export is fine if the destination file already exists
File f = new File(mDestinationPath);
if (f.isFile()) {
if (AdtPlugin.displayPrompt("Export Wizard",
"File already exists. Do you want to overwrite it?") == false) {
return false;
}
}
// save the properties
ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore);
ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias);
ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, mDestinationPath);
ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION,
mDestinationParentFolder.getAbsolutePath());
ProjectHelper.saveStringProperty(mProject, PROPERTY_FILENAME,
mApkMap.get(null)[APK_FILE_DEST]);
// run the export in an UI runnable.
IWorkbench workbench = PlatformUI.getWorkbench();
final boolean[] result = new boolean[1];
try {
workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
public void run(IProgressMonitor monitor) throws InvocationTargetException,
InterruptedException {
try {
result[0] = doExport(monitor);
} finally {
monitor.done();
}
}
});
} catch (InvocationTargetException e) {
return false;
} catch (InterruptedException e) {
return false;
}
return result[0];
}
private boolean doExport(IProgressMonitor monitor) {
try {
// first we make sure the project is built
mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
// if needed, create the keystore and/or key.
if (mKeystoreCreationMode || mKeyCreationMode) {
final ArrayList<String> output = new ArrayList<String>();
if (KeystoreHelper.createNewStore(
boolean createdStore = KeystoreHelper.createNewStore(
mKeystore,
null /*storeType*/,
mKeystorePassword,
@@ -218,7 +256,9 @@ public final class ExportWizard extends Wizard implements IExportWizard {
public void out(String message) {
output.add(message);
}
}) == false) {
});
if (createdStore == false) {
// keystore creation error!
displayError(output.toArray(new String[output.size()]));
return false;
@@ -245,20 +285,42 @@ public final class ExportWizard extends Wizard implements IExportWizard {
// check the private key/certificate again since it may have been created just above.
if (mPrivateKey != null && mCertificate != null) {
FileOutputStream fos = new FileOutputStream(mDestinationPath);
// get the output folder of the project to export.
// this is where we'll find the built apks to resign and export.
IFolder outputIFolder = BaseProjectHelper.getOutputFolder(mProject);
if (outputIFolder == null) {
return false;
}
String outputOsPath = outputIFolder.getLocation().toOSString();
// now generate the packages.
Set<Entry<String, String[]>> set = mApkMap.entrySet();
for (Entry<String, String[]> entry : set) {
String[] defaultApk = entry.getValue();
String srcFilename = defaultApk[APK_FILE_SOURCE];
String destFilename = defaultApk[APK_FILE_DEST];
FileOutputStream fos = new FileOutputStream(
new File(mDestinationParentFolder, destFilename));
SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate);
// get the input file.
FileInputStream fis = new FileInputStream(mApkFilePath);
FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename));
// add the content of the source file to the output file, and sign it at
// the same time.
try {
builder.writeZip(fis, null /* filter */);
} finally {
fis.close();
}
// close the builder: write the final signature files, and close the archive.
builder.close();
} finally {
try {
fis.close();
} finally {
fos.close();
}
}
}
return true;
}
} catch (FileNotFoundException e) {
@@ -271,6 +333,8 @@ public final class ExportWizard extends Wizard implements IExportWizard {
displayError(e);
} catch (KeytoolException e) {
displayError(e);
} catch (CoreException e) {
displayError(e);
}
return false;
@@ -282,10 +346,10 @@ public final class ExportWizard extends Wizard implements IExportWizard {
// a private key/certificate or the creation mode. In creation mode, unless
// all the key/keystore info is valid, the user cannot reach the last page, so there's
// no need to check them again here.
return mApkFilePath != null &&
return mApkMap != null && mApkMap.size() > 0 &&
((mPrivateKey != null && mCertificate != null)
|| mKeystoreCreationMode || mKeyCreationMode) &&
mDestinationPath != null;
mDestinationParentFolder != null;
}
/*
@@ -334,18 +398,12 @@ public final class ExportWizard extends Wizard implements IExportWizard {
return mProject;
}
void setProject(IProject project, String apkFilePath, String filename) {
void setProject(IProject project) {
mProject = project;
mApkFilePath = apkFilePath;
mApkFileName = filename;
updatePageOnChange(ExportWizardPage.DATA_PROJECT);
}
String getApkFilename() {
return mApkFileName;
}
void setKeystore(String path) {
mKeystore = path;
mPrivateKey = null;
@@ -444,8 +502,14 @@ public final class ExportWizard extends Wizard implements IExportWizard {
mCertificate = certificate;
}
void setDestination(String path) {
mDestinationPath = path;
void setDestination(File parentFolder, Map<String, String[]> apkMap) {
mDestinationParentFolder = parentFolder;
mApkMap = apkMap;
}
void resetDestination() {
mDestinationParentFolder = null;
mApkMap = null;
}
void updatePageOnChange(int changeMask) {
@@ -484,7 +548,7 @@ public final class ExportWizard extends Wizard implements IExportWizard {
* <p/>If no Throwable in the chain has a valid message, the canonical name of the first
* exception is returned.
*/
private static String getExceptionMessage(Throwable t) {
static String getExceptionMessage(Throwable t) {
String message = t.getMessage();
if (message == null) {
Throwable cause = t.getCause();

View File

@@ -18,13 +18,18 @@ package com.android.ide.eclipse.adt.project.export;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage;
import com.android.ide.eclipse.adt.sdk.Sdk;
import org.eclipse.core.resources.IProject;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
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.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
@@ -47,6 +52,10 @@ import java.security.KeyStore.PrivateKeyEntry;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* Final page of the wizard that checks the key and ask for the ouput location.
@@ -59,6 +68,12 @@ final class KeyCheckPage extends ExportWizardPage {
private Text mDestination;
private boolean mFatalSigningError;
private FormText mDetailText;
/** The Apk Config map for the current project */
private Map<String, String> mApkConfig;
private ScrolledComposite mScrolledComposite;
private String mKeyDetails;
private String mDestinationDetails;
protected KeyCheckPage(ExportWizard wizard, String pageName) {
super(pageName);
@@ -86,7 +101,7 @@ final class KeyCheckPage extends ExportWizardPage {
mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
mDestination.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
onDestinationChange();
onDestinationChange(false /*forceDetailUpdate*/);
}
});
final Button browseButton = new Button(composite, SWT.PUSH);
@@ -97,7 +112,10 @@ final class KeyCheckPage extends ExportWizardPage {
FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
fileDialog.setText("Destination file name");
fileDialog.setFileName(mWizard.getApkFilename());
// get a default apk name based on the project
String filename = ProjectHelper.getApkFilename(mWizard.getProject(),
null /*config*/);
fileDialog.setFileName(filename);
String saveLocation = fileDialog.open();
if (saveLocation != null) {
@@ -106,9 +124,21 @@ final class KeyCheckPage extends ExportWizardPage {
}
});
mDetailText = new FormText(composite, SWT.NONE);
mDetailText.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL);
mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
gd.horizontalSpan = 3;
mScrolledComposite.setExpandHorizontal(true);
mScrolledComposite.setExpandVertical(true);
mDetailText = new FormText(mScrolledComposite, SWT.NONE);
mScrolledComposite.setContent(mDetailText);
mScrolledComposite.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
updateScrolling();
}
});
setControl(composite);
}
@@ -119,11 +149,14 @@ final class KeyCheckPage extends ExportWizardPage {
if ((mProjectDataChanged & DATA_PROJECT) != 0) {
// reset the destination from the content of the project
IProject project = mWizard.getProject();
mApkConfig = Sdk.getCurrent().getProjectApkConfigs(project);
String destination = ProjectHelper.loadStringProperty(project,
ExportWizard.PROPERTY_DESTINATION);
if (destination != null) {
mDestination.setText(destination);
String filename = ProjectHelper.loadStringProperty(project,
ExportWizard.PROPERTY_FILENAME);
if (destination != null && filename != null) {
mDestination.setText(destination + File.separator + filename);
}
}
@@ -134,11 +167,14 @@ final class KeyCheckPage extends ExportWizardPage {
// reset the wizard with no key/cert to make it not finishable, unless a valid
// key/cert is found.
mWizard.setSigningInfo(null, null);
mPrivateKey = null;
mCertificate = null;
mKeyDetails = null;
if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) {
int validity = mWizard.getValidity();
StringBuilder sb = new StringBuilder(
String.format("<form><p>Certificate expires in %d years.</p>",
String.format("<p>Certificate expires in %d years.</p>",
validity));
if (validity < 25) {
@@ -149,8 +185,7 @@ final class KeyCheckPage extends ExportWizardPage {
sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
}
sb.append("</form>");
mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */);
mKeyDetails = sb.toString();
} else {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
@@ -192,10 +227,9 @@ final class KeyCheckPage extends ExportWizardPage {
Calendar today = Calendar.getInstance();
if (expirationCalendar.before(today)) {
mDetailText.setText(String.format(
"<form><p>Certificate expired on %s</p></form>",
mCertificate.getNotAfter().toString()),
true /* parseTags */, true /* expandURLs */);
mKeyDetails = String.format(
"<p>Certificate expired on %s</p>",
mCertificate.getNotAfter().toString());
// fatal error = nothing can make the page complete.
mFatalSigningError = true;
@@ -207,7 +241,7 @@ final class KeyCheckPage extends ExportWizardPage {
mWizard.setSigningInfo(mPrivateKey, mCertificate);
StringBuilder sb = new StringBuilder(String.format(
"<form><p>Certificate expires on %s.</p>",
"<p>Certificate expires on %s.</p>",
mCertificate.getNotAfter().toString()));
int expirationYear = expirationCalendar.get(Calendar.YEAR);
@@ -232,11 +266,8 @@ final class KeyCheckPage extends ExportWizardPage {
sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
}
sb.append("</form>");
mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */);
mKeyDetails = sb.toString();
}
mDetailText.getParent().layout();
} else {
// fatal error = nothing can make the page complete.
mFatalSigningError = true;
@@ -244,10 +275,15 @@ final class KeyCheckPage extends ExportWizardPage {
}
}
onDestinationChange();
onDestinationChange(true /*forceDetailUpdate*/);
}
private void onDestinationChange() {
/**
* Callback for destination field edition
* @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal
* error has happened in the signing.
*/
private void onDestinationChange(boolean forceDetailUpdate) {
if (mFatalSigningError == false) {
// reset messages for now.
setErrorMessage(null);
@@ -257,7 +293,8 @@ final class KeyCheckPage extends ExportWizardPage {
if (path.length() == 0) {
setErrorMessage("Enter destination for the APK file.");
mWizard.setDestination(null); // this is to reset canFinish in the wizard
// reset canFinish in the wizard.
mWizard.resetDestination();
setPageComplete(false);
return;
}
@@ -265,27 +302,140 @@ final class KeyCheckPage extends ExportWizardPage {
File file = new File(path);
if (file.isDirectory()) {
setErrorMessage("Destination is a directory.");
mWizard.setDestination(null); // this is to reset canFinish in the wizard
// reset canFinish in the wizard.
mWizard.resetDestination();
setPageComplete(false);
return;
}
File parentFile = file.getParentFile();
if (parentFile == null || parentFile.isDirectory() == false) {
File parentFolder = file.getParentFile();
if (parentFolder == null || parentFolder.isDirectory() == false) {
setErrorMessage("Not a valid directory.");
mWizard.setDestination(null); // this is to reset canFinish in the wizard
// reset canFinish in the wizard.
mWizard.resetDestination();
setPageComplete(false);
return;
}
// display the list of files that will actually be created
Map<String, String[]> apkFileMap = getApkFileMap(file);
// display them
boolean fileExists = false;
StringBuilder sb = new StringBuilder(String.format(
"<p>This will create the following files:</p>"));
Set<Entry<String, String[]>> set = apkFileMap.entrySet();
for (Entry<String, String[]> entry : set) {
String[] apkArray = entry.getValue();
String filename = apkArray[ExportWizard.APK_FILE_DEST];
File f = new File(parentFolder, filename);
if (f.isFile()) {
fileExists = true;
sb.append(String.format("<li>%1$s (WARNING: already exists)</li>", filename));
} else if (f.isDirectory()) {
setErrorMessage(String.format("%1$s is a directory.", filename));
// reset canFinish in the wizard.
mWizard.resetDestination();
setPageComplete(false);
return;
} else {
sb.append(String.format("<li>%1$s</li>", filename));
}
}
mDestinationDetails = sb.toString();
// no error, set the destination in the wizard.
mWizard.setDestination(path);
mWizard.setDestination(parentFolder, apkFileMap);
setPageComplete(true);
// However, we should also test if the file already exists.
if (file.isFile()) {
setMessage("Destination file already exists.", WARNING);
if (fileExists) {
setMessage("A destination file already exists.", WARNING);
}
updateDetailText();
} else if (forceDetailUpdate) {
updateDetailText();
}
}
/**
* Updates the scrollbar to match the content of the {@link FormText} or the new size
* of the {@link ScrolledComposite}.
*/
private void updateScrolling() {
if (mDetailText != null) {
Rectangle r = mScrolledComposite.getClientArea();
mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT));
mScrolledComposite.layout();
}
}
private void updateDetailText() {
StringBuilder sb = new StringBuilder("<form>");
if (mKeyDetails != null) {
sb.append(mKeyDetails);
}
if (mDestinationDetails != null && mFatalSigningError == false) {
sb.append(mDestinationDetails);
}
sb.append("</form>");
mDetailText.setText(sb.toString(), true /* parseTags */,
true /* expandURLs */);
mDetailText.getParent().layout();
updateScrolling();
}
/**
* Creates the list of destination filenames based on the content of the destination field
* and the list of APK configurations for the project.
* @param file
* @return
*/
private Map<String, String[]> getApkFileMap(File file) {
String filename = file.getName();
HashMap<String, String[]> map = new HashMap<String, String[]>();
// add the default APK filename
String[] apkArray = new String[ExportWizard.APK_COUNT];
apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename(
mWizard.getProject(), null /*config*/);
apkArray[ExportWizard.APK_FILE_DEST] = filename;
map.put(null, apkArray);
// add the APKs for each APK configuration.
if (mApkConfig != null && mApkConfig.size() > 0) {
// remove the extension.
int index = filename.lastIndexOf('.');
String base = filename.substring(0, index);
String extension = filename.substring(index);
Set<Entry<String, String>> set = mApkConfig.entrySet();
for (Entry<String, String> entry : set) {
apkArray = new String[ExportWizard.APK_COUNT];
apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename(
mWizard.getProject(), entry.getKey());
apkArray[ExportWizard.APK_FILE_DEST] = base + "-" + entry.getKey() + extension;
map.put(entry.getKey(), apkArray);
}
}
return map;
}
@Override
protected void onException(Throwable t) {
super.onException(t);
mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));
}
}

View File

@@ -266,7 +266,7 @@ final class ProjectCheckPage extends ExportWizardPage {
}
// update the wizard with the new project
mWizard.setProject(null, null, null);
mWizard.setProject(null);
//test the project name first!
String text = mProjectText.getText().trim();
@@ -289,7 +289,7 @@ final class ProjectCheckPage extends ExportWizardPage {
setErrorMessage(null);
// update the wizard with the new project
setApkFilePathInWizard(found);
mWizard.setProject(found);
// now rebuild the error ui.
buildErrorUi(found);
@@ -299,24 +299,4 @@ final class ProjectCheckPage extends ExportWizardPage {
}
}
}
private void setApkFilePathInWizard(IProject project) {
if (project != null) {
IFolder outputIFolder = BaseProjectHelper.getOutputFolder(project);
if (outputIFolder != null) {
String outputOsPath = outputIFolder.getLocation().toOSString();
String apkFilePath = outputOsPath + File.separator + project.getName() +
AndroidConstants.DOT_ANDROID_PACKAGE;
File f = new File(apkFilePath);
if (f.isFile()) {
mWizard.setProject(project, apkFilePath, f.getName());
return;
}
}
}
mWizard.setProject(null, null, null);
}
}

View File

@@ -352,12 +352,18 @@ public class NewProjectWizard extends Wizard implements INewWizard {
// Create the resource folders in the project if they don't already exist.
addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor);
// Setup class path
// Setup class path: mark folders as source folders
IJavaProject javaProject = JavaCore.create(project);
for (String sourceFolder : sourceFolders) {
setupSourceFolder(javaProject, sourceFolder, monitor);
}
// Mark the gen source folder as derived
IFolder genSrcFolder = project.getFolder(AndroidConstants.WS_ROOT + GEN_SRC_DIRECTORY);
if (genSrcFolder.exists()) {
genSrcFolder.setDerived(true);
}
if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) {
// Create files in the project if they don't already exist
addManifest(project, parameters, stringDictionary, monitor);

View File

@@ -328,7 +328,7 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor {
}
if (currAttrNode != null) {
choices = currAttrNode.getPossibleValues();
choices = currAttrNode.getPossibleValues(value);
if (currAttrNode instanceof UiFlagAttributeNode) {
// A "flag" can consist of several values separated by "or" (|).

View File

@@ -616,7 +616,7 @@ public class UiClassAttributeNode extends UiTextAttributeNode {
}
@Override
public String[] getPossibleValues() {
public String[] getPossibleValues(String prefix) {
// TODO: compute a list of existing classes for content assist completion
return null;
}

View File

@@ -311,7 +311,7 @@ public class UiPackageAttributeNode extends UiTextAttributeNode {
}
@Override
public String[] getPossibleValues() {
public String[] getPossibleValues(String prefix) {
// TODO: compute a list of existing packages for content assist completion
return null;
}

View File

@@ -48,7 +48,7 @@ public class ListValueCellEditor extends ComboBoxCellEditor {
UiListAttributeNode uiListAttribute = (UiListAttributeNode)value;
// set the possible values in the combo
String[] items = uiListAttribute.getPossibleValues();
String[] items = uiListAttribute.getPossibleValues(null);
mItems = new String[items.length];
System.arraycopy(items, 0, mItems, 0, items.length);
setItems(mItems);

View File

@@ -117,9 +117,13 @@ public abstract class UiAttributeNode {
* <p/>
* Implementations that do not have any known values should return null.
*
* @return A list of possible completion values or null.
* @param prefix An optional prefix string, which is whatever the user has already started
* typing. Can be null or an empty string. The implementation can use this to filter choices
* and only return strings that match this prefix. A lazy or default implementation can
* simply ignore this and return everything.
* @return A list of possible completion values, and empty array or null.
*/
public abstract String[] getPossibleValues();
public abstract String[] getPossibleValues(String prefix);
/**
* Called when the XML is being loaded or has changed to

View File

@@ -124,9 +124,11 @@ public class UiFlagAttributeNode extends UiTextAttributeNode {
/**
* Get the flag names, either from the initial names set in the attribute
* or by querying the framework resource parser.
*
* {@inheritDoc}
*/
@Override
public String[] getPossibleValues() {
public String[] getPossibleValues(String prefix) {
String attr_name = getDescriptor().getXmlLocalName();
String element_name = getUiParent().getDescriptor().getXmlName();
@@ -242,7 +244,7 @@ public class UiFlagAttributeNode extends UiTextAttributeNode {
final TableColumn column = new TableColumn(mTable, SWT.NONE);
// List all the expected flag names and check those which are currently used
String[] names = getPossibleValues();
String[] names = getPossibleValues(null);
if (names != null) {
for (String name : names) {
TableItem item = new TableItem(mTable, SWT.NONE);

View File

@@ -108,7 +108,7 @@ public class UiListAttributeNode extends UiAbstractTextAttributeNode {
}
protected void fillCombo() {
String[] values = getPossibleValues();
String[] values = getPossibleValues(null);
if (values == null) {
AdtPlugin.log(IStatus.ERROR,
@@ -124,9 +124,11 @@ public class UiListAttributeNode extends UiAbstractTextAttributeNode {
/**
* Get the list values, either from the initial values set in the attribute
* or by querying the framework resource parser.
*
* {@inheritDoc}
*/
@Override
public String[] getPossibleValues() {
public String[] getPossibleValues(String prefix) {
AttributeDescriptor descriptor = getDescriptor();
UiElementNode uiParent = getUiParent();
@@ -134,13 +136,13 @@ public class UiListAttributeNode extends UiAbstractTextAttributeNode {
String element_name = uiParent.getDescriptor().getXmlName();
// FrameworkResourceManager expects a specific prefix for the attribute.
String prefix = "";
String nsPrefix = "";
if (SdkConstants.NS_RESOURCES.equals(descriptor.getNamespaceUri())) {
prefix = "android:"; //$NON-NLS-1$
nsPrefix = "android:"; //$NON-NLS-1$
} else if (XmlnsAttributeDescriptor.XMLNS_URI.equals(descriptor.getNamespaceUri())) {
prefix = "xmlns:"; //$NON-NLS-1$
nsPrefix = "xmlns:"; //$NON-NLS-1$
}
attr_name = prefix + attr_name;
attr_name = nsPrefix + attr_name;
String[] values = null;

View File

@@ -18,6 +18,7 @@ package com.android.ide.eclipse.editors.uimodel;
import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
import com.android.ide.eclipse.common.resources.IResourceRepository;
import com.android.ide.eclipse.common.resources.ResourceItem;
import com.android.ide.eclipse.common.resources.ResourceType;
import com.android.ide.eclipse.editors.AndroidEditor;
import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
@@ -44,6 +45,10 @@ import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.TableWrapData;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents an XML attribute for a resource that can be modified using a simple text field or
* a dialog to choose an existing resource.
@@ -154,8 +159,81 @@ public class UiResourceAttributeNode extends UiTextAttributeNode {
}
@Override
public String[] getPossibleValues() {
// TODO: compute a list of existing resources for content assist completion
return null;
public String[] getPossibleValues(String prefix) {
IResourceRepository repository = null;
boolean isSystem = false;
UiElementNode uiNode = getUiParent();
AndroidEditor editor = uiNode.getEditor();
if (prefix == null || prefix.indexOf("android:") < 0) {
IProject project = editor.getProject();
if (project != null) {
// get the resource repository for this project and the system resources.
repository = ResourceManager.getInstance().getProjectResources(project);
}
} else {
// If there's a prefix with "android:" in it, use the system resources
AndroidTargetData data = editor.getTargetData();
repository = data.getSystemResources();
isSystem = true;
}
// Get list of potential resource types, either specific to this project
// or the generic list.
ResourceType[] resTypes = (repository != null) ?
repository.getAvailableResourceTypes() :
ResourceType.values();
// Get the type name from the prefix, if any. It's any word before the / if there's one
String typeName = null;
if (prefix != null) {
Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix);
if (m.matches()) {
typeName = m.group(1);
}
}
// Now collect results
ArrayList<String> results = new ArrayList<String>();
if (typeName == null) {
// This prefix does not have a / in it, so the resource string is either empty
// or does not have the resource type in it. Simply offer the list of potential
// resource types.
for (ResourceType resType : resTypes) {
results.add("@" + resType.getName() + "/");
if (resType == ResourceType.ID) {
// Also offer the + version to create an id from scratch
results.add("@+" + resType.getName() + "/");
}
}
} else if (repository != null) {
// We have a style name and a repository. Find all resources that match this
// type and recreate suggestions out of them.
ResourceType resType = ResourceType.getEnum(typeName);
if (resType != null) {
StringBuilder sb = new StringBuilder();
sb.append('@');
if (prefix.indexOf('+') >= 0) {
sb.append('+');
}
if (isSystem) {
sb.append("android:");
}
sb.append(typeName).append('/');
String base = sb.toString();
for (ResourceItem item : repository.getResources(resType)) {
results.add(base + item.getName());
}
}
}
return results.toArray(new String[results.size()]);
}
}

View File

@@ -96,9 +96,13 @@ public class UiSeparatorAttributeNode extends UiAttributeNode {
sep.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
}
/** No completion values for this UI attribute. */
/**
* No completion values for this UI attribute.
*
* {@inheritDoc}
*/
@Override
public String[] getPossibleValues() {
public String[] getPossibleValues(String prefix) {
return null;
}

View File

@@ -70,9 +70,13 @@ public class UiTextAttributeNode extends UiAbstractTextAttributeNode {
setTextWidget(text);
}
/** No completion values for this UI attribute. */
/**
* No completion values for this UI attribute.
*
* {@inheritDoc}
*/
@Override
public String[] getPossibleValues() {
public String[] getPossibleValues(String prefix) {
return null;
}

View File

@@ -25,6 +25,8 @@ import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Set;
class LayoutRenderer extends JComponent {
@@ -34,14 +36,23 @@ class LayoutRenderer extends JComponent {
private boolean showExtras;
private ViewHierarchyScene scene;
private JComponent sceneView;
LayoutRenderer(ViewHierarchyScene scene) {
LayoutRenderer(ViewHierarchyScene scene, JComponent sceneView) {
this.scene = scene;
this.sceneView = sceneView;
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0));
setBackground(Color.BLACK);
setForeground(Color.WHITE);
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
selectChild(event.getX(), event.getY());
}
});
}
@Override
@@ -118,4 +129,49 @@ class LayoutRenderer extends JComponent {
this.showExtras = showExtras;
repaint();
}
private void selectChild(int x, int y) {
if (scene == null) {
return;
}
ViewNode root = scene.getRoot();
if (root == null) {
return;
}
Insets insets = getInsets();
int xoffset = (getWidth() - insets.left - insets.right - root.width) / 2 + insets.left + 1;
int yoffset = (getHeight() - insets.top - insets.bottom - root.height) / 2 + insets.top + 1;
x -= xoffset;
y -= yoffset;
if (x >= 0 && x < EMULATED_SCREEN_WIDTH && y >= 0 && y < EMULATED_SCREEN_HEIGHT) {
ViewNode hit = findChild(root, root, x, y);
scene.setFocusedObject(hit);
sceneView.repaint();
}
}
private ViewNode findChild(ViewNode root, ViewNode besthit, int x, int y) {
ViewNode hit = besthit;
for (ViewNode node : root.children) {
if (node.left <= x && x < node.left + node.width &&
node.top <= y && y < node.top + node.height) {
if (node.width <= hit.width && node.height <= hit.height) {
hit = node;
}
}
if (node.children.size() > 0) {
hit = findChild(node, hit,
x - (node.left - node.parent.scrollX),
y - (node.top - node.parent.scrollY));
}
}
return hit;
}
}

View File

@@ -45,6 +45,8 @@ import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.concurrent.ExecutionException;
class ScreenViewer extends JPanel implements ActionListener {
@@ -69,6 +71,8 @@ class ScreenViewer extends JPanel implements ActionListener {
private Timer timer;
private ViewNode node;
private JSlider zoomSlider;
ScreenViewer(Workspace workspace, Device device, int spacing) {
setLayout(new BorderLayout());
setOpaque(false);
@@ -95,6 +99,7 @@ class ScreenViewer extends JPanel implements ActionListener {
private JPanel buildLoupePanel(int spacing) {
loupe = new LoupeViewer();
loupe.addMouseWheelListener(new WheelZoomListener());
CrosshairPanel crosshairPanel = new CrosshairPanel(loupe);
JPanel loupePanel = new JPanel(new BorderLayout());
@@ -106,9 +111,20 @@ class ScreenViewer extends JPanel implements ActionListener {
return loupePanel;
}
private class WheelZoomListener implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent e) {
if (zoomSlider != null) {
int val = zoomSlider.getValue();
val -= e.getWheelRotation() * 2;
zoomSlider.setValue(val);
}
}
}
private JPanel buildViewerAndControls() {
JPanel panel = new JPanel(new GridBagLayout());
crosshair = new Crosshair(new ScreenshotViewer());
crosshair.addMouseWheelListener(new WheelZoomListener());
panel.add(crosshair,
new GridBagConstraints(0, y++, 2, 1, 1.0f, 0.0f,
GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE,
@@ -131,7 +147,8 @@ class ScreenViewer extends JPanel implements ActionListener {
timer.restart();
}
});
buildSlider(panel, "Zoom:", "2x", "24x", 2, 24, 8, 2).addChangeListener(
zoomSlider = buildSlider(panel, "Zoom:", "2x", "24x", 2, 24, 8, 2);
zoomSlider.addChangeListener(
new ChangeListener() {
public void stateChanged(ChangeEvent event) {
zoom = ((JSlider) event.getSource()).getValue();

View File

@@ -67,6 +67,7 @@ import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JScrollBar;
import javax.swing.JSlider;
import javax.swing.JSplitPane;
import javax.swing.JTable;
@@ -105,6 +106,8 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -173,6 +176,7 @@ public class Workspace extends JFrame {
add(buildMainPanel());
setJMenuBar(buildMenuBar());
devices.changeSelection(0, 0, false, false);
currentDeviceChanged();
pack();
@@ -648,6 +652,7 @@ public class Workspace extends JFrame {
sceneView = scene.createView();
sceneView.addMouseListener(new NodeClickListener());
sceneView.addMouseWheelListener(new WheelZoomListener());
sceneScroller.setViewportView(sceneView);
if (extrasPanel != null) {
@@ -678,7 +683,10 @@ public class Workspace extends JFrame {
private JPanel buildExtrasPanel() {
extrasPanel = new JPanel(new BorderLayout());
extrasPanel.add(new JScrollPane(layoutView = new LayoutRenderer(scene)));
JScrollPane p = new JScrollPane(layoutView = new LayoutRenderer(scene, sceneView));
JScrollBar b = p.getVerticalScrollBar();
b.setUnitIncrement(10);
extrasPanel.add(p);
extrasPanel.add(scene.createSatelliteView(), BorderLayout.SOUTH);
extrasPanel.add(buildLayoutViewControlButtons(), BorderLayout.NORTH);
return extrasPanel;
@@ -1231,6 +1239,15 @@ public class Workspace extends JFrame {
}
}
private class WheelZoomListener implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent e) {
if (zoomSlider != null) {
int val = zoomSlider.getValue();
val -= e.getWheelRotation() * 10;
zoomSlider.setValue(val);
}
}
}
private class DevicesTableModel extends DefaultTableModel implements
AndroidDebugBridge.IDeviceChangeListener {

View File

@@ -48,11 +48,49 @@ public final class AvdManager {
private final static String AVD_INFO_PATH = "path";
private final static String AVD_INFO_TARGET = "target";
/**
* 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 final static String AVD_INI_SKIN_PATH = "skin.path";
/**
* 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 final static String AVD_INI_SKIN_NAME = "skin.name";
/**
* 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 final static String AVD_INI_SDCARD_PATH = "sdcard.path";
/**
* 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
*/
public final static String AVD_INI_SDCARD_SIZE = "sdcard.size";
/**
* 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 final static String AVD_INI_IMAGES_1 = "image.sysdir.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 final static String AVD_INI_IMAGES_2 = "image.sysdir.2";
/**
@@ -69,6 +107,9 @@ public final class AvdManager {
private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + INI_EXTENSION + "$",
Pattern.CASE_INSENSITIVE);
/**
* Pattern for matching SD Card sizes, e.g. "4K" or "16M".
*/
private final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("\\d+[MK]?");
/** An immutable structure describing an Android Virtual Device. */