Home | History | Annotate | Download | only in widgets
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.sdkuilib.internal.widgets;
     18 
     19 import com.android.sdklib.internal.avd.AvdInfo;
     20 import com.android.sdklib.internal.avd.AvdManager;
     21 import com.android.sdkuilib.internal.repository.SettingsController;
     22 import com.android.sdkuilib.ui.GridDialog;
     23 
     24 import org.eclipse.jface.dialogs.IDialogConstants;
     25 import org.eclipse.jface.window.Window;
     26 import org.eclipse.swt.SWT;
     27 import org.eclipse.swt.events.ModifyEvent;
     28 import org.eclipse.swt.events.ModifyListener;
     29 import org.eclipse.swt.events.SelectionAdapter;
     30 import org.eclipse.swt.events.SelectionEvent;
     31 import org.eclipse.swt.events.VerifyEvent;
     32 import org.eclipse.swt.events.VerifyListener;
     33 import org.eclipse.swt.layout.GridData;
     34 import org.eclipse.swt.layout.GridLayout;
     35 import org.eclipse.swt.widgets.Button;
     36 import org.eclipse.swt.widgets.Composite;
     37 import org.eclipse.swt.widgets.Control;
     38 import org.eclipse.swt.widgets.Group;
     39 import org.eclipse.swt.widgets.Label;
     40 import org.eclipse.swt.widgets.Shell;
     41 import org.eclipse.swt.widgets.Text;
     42 
     43 import java.awt.Toolkit;
     44 import java.io.BufferedReader;
     45 import java.io.File;
     46 import java.io.FileReader;
     47 import java.io.IOException;
     48 import java.util.HashMap;
     49 import java.util.Map;
     50 import java.util.regex.Matcher;
     51 import java.util.regex.Pattern;
     52 
     53 /**
     54  * Dialog dealing with emulator launch options. The following options are supported:
     55  * <ul>
     56  * <li>-wipe-data</li>
     57  * <li>-scale</li>
     58  * </ul>
     59  * Values are stored (in the class as static field) to be reused while the app is still running.
     60  * The Monitor dpi is stored in the settings if available.
     61  */
     62 final class AvdStartDialog extends GridDialog {
     63     // static field to reuse values during the same session.
     64     private static boolean sWipeData = false;
     65     private static boolean sSnapshotSave = true;
     66     private static boolean sSnapshotLaunch = true;
     67     private static int sMonitorDpi = 72; // used if there's no setting controller.
     68     private static final Map<String, String> sSkinScaling = new HashMap<String, String>();
     69 
     70     private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?");
     71 
     72     private final AvdInfo mAvd;
     73     private final String mSdkLocation;
     74     private final SettingsController mSettingsController;
     75 
     76     private Text mScreenSize;
     77     private Text mMonitorDpi;
     78     private Button mScaleButton;
     79 
     80     private float mScale = 0.f;
     81     private boolean mWipeData = false;
     82     private int mDensity = 160; // medium density
     83     private int mSize1 = -1;
     84     private int mSize2 = -1;
     85     private String mSkinDisplay;
     86     private boolean mEnableScaling = true;
     87     private Label mScaleField;
     88     private boolean mHasSnapshot = true;
     89     private boolean mSnapshotSave = true;
     90     private boolean mSnapshotLaunch = true;
     91     private Button mSnapshotLaunchCheckbox;
     92 
     93     AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation,
     94             SettingsController settingsController) {
     95         super(parentShell, 2, false);
     96         mAvd = avd;
     97         mSdkLocation = sdkLocation;
     98         mSettingsController = settingsController;
     99         if (mAvd == null) {
    100             throw new IllegalArgumentException("avd cannot be null");
    101         }
    102         if (mSdkLocation == null) {
    103             throw new IllegalArgumentException("sdkLocation cannot be null");
    104         }
    105 
    106         computeSkinData();
    107     }
    108 
    109     public boolean hasWipeData() {
    110         return mWipeData;
    111     }
    112 
    113     /**
    114      * Returns the scaling factor, or 0.f if none are set.
    115      */
    116     public float getScale() {
    117         return mScale;
    118     }
    119 
    120     @Override
    121     public void createDialogContent(final Composite parent) {
    122         GridData gd;
    123 
    124         Label l = new Label(parent, SWT.NONE);
    125         l.setText("Skin:");
    126 
    127         l = new Label(parent, SWT.NONE);
    128         l.setText(mSkinDisplay == null ? "None" : mSkinDisplay);
    129         l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    130 
    131         l = new Label(parent, SWT.NONE);
    132         l.setText("Density:");
    133 
    134         l = new Label(parent, SWT.NONE);
    135         l.setText(getDensityText());
    136         l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    137 
    138         mScaleButton = new Button(parent, SWT.CHECK);
    139         mScaleButton.setText("Scale display to real size");
    140         mScaleButton.setEnabled(mEnableScaling);
    141         boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null;
    142         mScaleButton.setSelection(defaultState);
    143         mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    144         gd.horizontalSpan = 2;
    145         final Group scaleGroup = new Group(parent, SWT.NONE);
    146         scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    147         gd.horizontalIndent = 30;
    148         gd.horizontalSpan = 2;
    149         scaleGroup.setLayout(new GridLayout(3, false));
    150 
    151         l = new Label(scaleGroup, SWT.NONE);
    152         l.setText("Screen Size (in):");
    153         mScreenSize = new Text(scaleGroup, SWT.BORDER);
    154         mScreenSize.setText(getScreenSize());
    155         mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    156         mScreenSize.addVerifyListener(new VerifyListener() {
    157             @Override
    158             public void verifyText(VerifyEvent event) {
    159                 // combine the current content and the new text
    160                 String text = mScreenSize.getText();
    161                 text = text.substring(0, event.start) + event.text + text.substring(event.end);
    162 
    163                 // now make sure it's a match for the regex
    164                 event.doit = sScreenSizePattern.matcher(text).matches();
    165             }
    166         });
    167         mScreenSize.addModifyListener(new ModifyListener() {
    168             @Override
    169             public void modifyText(ModifyEvent event) {
    170                 onScaleChange();
    171             }
    172         });
    173 
    174         // empty composite, only 2 widgets on this line.
    175         new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData());
    176         gd.widthHint = gd.heightHint = 0;
    177 
    178         l = new Label(scaleGroup, SWT.NONE);
    179         l.setText("Monitor dpi:");
    180         mMonitorDpi = new Text(scaleGroup, SWT.BORDER);
    181         mMonitorDpi.setText(Integer.toString(getMonitorDpi()));
    182         mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    183         gd.widthHint = 50;
    184         mMonitorDpi.addVerifyListener(new VerifyListener() {
    185             @Override
    186             public void verifyText(VerifyEvent event) {
    187                 // check for digit only.
    188                 for (int i = 0 ; i < event.text.length(); i++) {
    189                     char letter = event.text.charAt(i);
    190                     if (letter < '0' || letter > '9') {
    191                         event.doit = false;
    192                         return;
    193                     }
    194                 }
    195             }
    196         });
    197         mMonitorDpi.addModifyListener(new ModifyListener() {
    198             @Override
    199             public void modifyText(ModifyEvent event) {
    200                 onScaleChange();
    201             }
    202         });
    203 
    204         Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT);
    205         button.setText("?");
    206         button.setToolTipText("Click to figure out your monitor's pixel density");
    207         button.addSelectionListener(new SelectionAdapter() {
    208             @Override
    209             public void widgetSelected(SelectionEvent arg0) {
    210                 ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell());
    211                 if (dialog.open() == Window.OK) {
    212                     mMonitorDpi.setText(Integer.toString(dialog.getDensity()));
    213                 }
    214             }
    215         });
    216 
    217         l = new Label(scaleGroup, SWT.NONE);
    218         l.setText("Scale:");
    219         mScaleField = new Label(scaleGroup, SWT.NONE);
    220         mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER,
    221                 true /*grabExcessHorizontalSpace*/,
    222                 true /*grabExcessVerticalSpace*/,
    223                 2 /*horizontalSpan*/,
    224                 1 /*verticalSpan*/));
    225         setScale(mScale); // set initial text value
    226 
    227         enableGroup(scaleGroup, defaultState);
    228 
    229         mScaleButton.addSelectionListener(new SelectionAdapter() {
    230             @Override
    231             public void widgetSelected(SelectionEvent event) {
    232                 boolean enabled = mScaleButton.getSelection();
    233                 enableGroup(scaleGroup, enabled);
    234                 if (enabled) {
    235                     onScaleChange();
    236                 } else {
    237                     setScale(0);
    238                 }
    239             }
    240         });
    241 
    242         final Button wipeButton = new Button(parent, SWT.CHECK);
    243         wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    244         gd.horizontalSpan = 2;
    245         wipeButton.setText("Wipe user data");
    246         wipeButton.setSelection(mWipeData = sWipeData);
    247         wipeButton.addSelectionListener(new SelectionAdapter() {
    248             @Override
    249             public void widgetSelected(SelectionEvent arg0) {
    250                 mWipeData = wipeButton.getSelection();
    251                 updateSnapshotLaunchAvailability();
    252             }
    253         });
    254 
    255         Map<String, String> prop = mAvd.getProperties();
    256         String snapshotPresent = prop.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
    257         mHasSnapshot = (snapshotPresent != null) && snapshotPresent.equals("true");
    258 
    259         mSnapshotLaunchCheckbox = new Button(parent, SWT.CHECK);
    260         mSnapshotLaunchCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    261         gd.horizontalSpan = 2;
    262         mSnapshotLaunchCheckbox.setText("Launch from snapshot");
    263         updateSnapshotLaunchAvailability();
    264         mSnapshotLaunchCheckbox.addSelectionListener(new SelectionAdapter() {
    265             @Override
    266             public void widgetSelected(SelectionEvent arg0) {
    267                 mSnapshotLaunch = mSnapshotLaunchCheckbox.getSelection();
    268             }
    269         });
    270 
    271         final Button snapshotSaveCheckbox = new Button(parent, SWT.CHECK);
    272         snapshotSaveCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    273         gd.horizontalSpan = 2;
    274         snapshotSaveCheckbox.setText("Save to snapshot");
    275         snapshotSaveCheckbox.setSelection((mSnapshotSave = sSnapshotSave) && mHasSnapshot);
    276         snapshotSaveCheckbox.setEnabled(mHasSnapshot);
    277         snapshotSaveCheckbox.addSelectionListener(new SelectionAdapter() {
    278             @Override
    279             public void widgetSelected(SelectionEvent arg0) {
    280                 mSnapshotSave = snapshotSaveCheckbox.getSelection();
    281             }
    282         });
    283 
    284         l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
    285         l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    286         gd.horizontalSpan = 2;
    287 
    288         // if the scaling is enabled by default, we must initialize the value of mScale
    289         if (defaultState) {
    290             onScaleChange();
    291         }
    292     }
    293 
    294     /** On Windows we need to manually enable/disable the children of a group */
    295     private void enableGroup(final Group group, boolean enabled) {
    296         group.setEnabled(enabled);
    297         for (Control c : group.getChildren()) {
    298             c.setEnabled(enabled);
    299         }
    300     }
    301 
    302     @Override
    303     protected void configureShell(Shell newShell) {
    304         super.configureShell(newShell);
    305         newShell.setText("Launch Options");
    306     }
    307 
    308     @Override
    309     protected Button createButton(Composite parent, int id, String label, boolean defaultButton) {
    310         if (id == IDialogConstants.OK_ID) {
    311             label = "Launch";
    312         }
    313 
    314         return super.createButton(parent, id, label, defaultButton);
    315     }
    316 
    317     @Override
    318     protected void okPressed() {
    319         // override ok to store some info
    320         // first the monitor dpi
    321         String dpi = mMonitorDpi.getText();
    322         if (dpi.length() > 0) {
    323             sMonitorDpi = Integer.parseInt(dpi);
    324 
    325             // if there is a setting controller, save it
    326             if (mSettingsController != null) {
    327                 mSettingsController.setMonitorDensity(sMonitorDpi);
    328                 mSettingsController.saveSettings();
    329             }
    330         }
    331 
    332         // now the scale factor
    333         String key = mAvd.getName();
    334         sSkinScaling.remove(key);
    335         if (mScaleButton.getSelection()) {
    336             String size = mScreenSize.getText();
    337             if (size.length() > 0) {
    338                 sSkinScaling.put(key, size);
    339             }
    340         }
    341 
    342         // and then the wipe-data checkbox
    343         sWipeData = mWipeData;
    344 
    345         // and the snapshot handling if those checkboxes are enabled.
    346         if (mHasSnapshot) {
    347             sSnapshotSave = mSnapshotSave;
    348             if (!mWipeData) {
    349                 sSnapshotLaunch = mSnapshotLaunch;
    350             }
    351         }
    352 
    353         // finally continue with the ok action
    354         super.okPressed();
    355     }
    356 
    357     private void computeSkinData() {
    358         Map<String, String> prop = mAvd.getProperties();
    359         String dpi = prop.get("hw.lcd.density");
    360         if (dpi != null && dpi.length() > 0) {
    361             mDensity  = Integer.parseInt(dpi);
    362         }
    363 
    364         findSkinResolution();
    365     }
    366 
    367     private void onScaleChange() {
    368         String sizeStr = mScreenSize.getText();
    369         if (sizeStr.length() == 0) {
    370             setScale(0);
    371             return;
    372         }
    373 
    374         String dpiStr = mMonitorDpi.getText();
    375         if (dpiStr.length() == 0) {
    376             setScale(0);
    377             return;
    378         }
    379 
    380         int dpi = Integer.parseInt(dpiStr);
    381         float size = Float.parseFloat(sizeStr);
    382         /*
    383          * We are trying to emulate the following device:
    384          * resolution: 'mSize1'x'mSize2'
    385          * density: 'mDensity'
    386          * screen diagonal: 'size'
    387          * ontop a monitor running at 'dpi'
    388          */
    389         // We start by computing the screen diagonal in pixels, if the density was really mDensity
    390         float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2);
    391         // Now we would convert this in actual inches:
    392         //    diagonalIn = diagonal / mDensity
    393         // the scale factor is a mix of adapting to the new density and to the new size.
    394         //    (size/diagonalIn) * (dpi/mDensity)
    395         // this can be simplified to:
    396         setScale((size * dpi) / diagonalPx);
    397     }
    398 
    399     private void setScale(float scale) {
    400         mScale = scale;
    401 
    402         // Do the rounding exactly like AvdSelector will do.
    403         scale = Math.round(scale * 100);
    404         scale /=  100.f;
    405 
    406         if (scale == 0.f) {
    407             mScaleField.setText("default");  //$NON-NLS-1$
    408         } else {
    409             mScaleField.setText(String.format("%.2f", scale));  //$NON-NLS-1$
    410         }
    411     }
    412 
    413     /**
    414      * Returns the monitor dpi to start with.
    415      * This can be coming from the settings, the session-based storage, or the from whatever Java
    416      * can tell us.
    417      */
    418     private int getMonitorDpi() {
    419         if (mSettingsController != null) {
    420             sMonitorDpi = mSettingsController.getMonitorDensity();
    421         }
    422 
    423         if (sMonitorDpi == -1) { // first time? try to get a value
    424             sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution();
    425         }
    426 
    427         return sMonitorDpi;
    428     }
    429 
    430     /**
    431      * Returns the screen size to start with.
    432      * <p/>If an emulator with the same skin was already launched, scaled, the size used is reused.
    433      * <p/>Otherwise the default is returned (3)
    434      */
    435     private String getScreenSize() {
    436         String size = sSkinScaling.get(mAvd.getName());
    437         if (size != null) {
    438             return size;
    439         }
    440 
    441         return "3";
    442     }
    443 
    444     /**
    445      * Returns a display string for the density.
    446      */
    447     private String getDensityText() {
    448         switch (mDensity) {
    449             case 120:
    450                 return "Low (120)";
    451             case 160:
    452                 return "Medium (160)";
    453             case 240:
    454                 return "High (240)";
    455         }
    456 
    457         return Integer.toString(mDensity);
    458     }
    459 
    460     /**
    461      * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}.
    462      */
    463     private void findSkinResolution() {
    464         Map<String, String> prop = mAvd.getProperties();
    465         String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME);
    466 
    467         if (skinName != null) {
    468             Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName);
    469             if (m != null && m.matches()) {
    470                 mSize1 = Integer.parseInt(m.group(1));
    471                 mSize2 = Integer.parseInt(m.group(2));
    472                 mSkinDisplay = skinName;
    473                 mEnableScaling = true;
    474                 return;
    475             }
    476         }
    477 
    478         // The resolution is inside the layout file of the skin.
    479         mEnableScaling = false; // default to false for now.
    480 
    481         // path to the skin layout file.
    482         String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH);
    483         if (skinPath != null) {
    484             File skinFolder = new File(mSdkLocation, skinPath);
    485             if (skinFolder.isDirectory()) {
    486                 File layoutFile = new File(skinFolder, "layout");
    487                 if (layoutFile.isFile()) {
    488                     if (parseLayoutFile(layoutFile)) {
    489                         mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2);
    490                         mEnableScaling = true;
    491                     } else {
    492                         mSkinDisplay = skinName;
    493                     }
    494                 }
    495             }
    496         }
    497     }
    498 
    499     /**
    500      * Parses a layout file.
    501      * <p/>
    502      * the format is relatively easy. It's a collection of items defined as
    503      * &lg;name&gt; {
    504      *     &lg;content&gt;
    505      * }
    506      *
    507      * content is either 1+ items or 1+ properties
    508      * properties are defined as
    509      * &lg;name&gt;&lg;whitespace&gt;&lg;value&gt;
    510      *
    511      * We're going to look for an item called display, with 2 properties height and width.
    512      * This is very basic parser.
    513      *
    514      * @param layoutFile the file to parse
    515      * @return true if both sizes where found.
    516      */
    517     private boolean parseLayoutFile(File layoutFile) {
    518         try {
    519             BufferedReader input = new BufferedReader(new FileReader(layoutFile));
    520             String line;
    521 
    522             while ((line = input.readLine()) != null) {
    523                 // trim to remove whitespace
    524                 line = line.trim();
    525                 int len = line.length();
    526                 if (len == 0) continue;
    527 
    528                 // check if this is a new item
    529                 if (line.charAt(len-1) == '{') {
    530                     // this is the start of a node
    531                     String[] tokens = line.split(" ");
    532                     if ("display".equals(tokens[0])) {
    533                         // this is the one we're looking for!
    534                         while ((mSize1 == -1 || mSize2 == -1) &&
    535                                 (line = input.readLine()) != null) {
    536                             // trim to remove whitespace
    537                             line = line.trim();
    538                             len = line.length();
    539                             if (len == 0) continue;
    540 
    541                             if ("}".equals(line)) { // looks like we're done with the item.
    542                                 break;
    543                             }
    544 
    545                             tokens = line.split(" ");
    546                             if (tokens.length >= 2) {
    547                                 // there can be multiple space between the name and value
    548                                 // in which case we'll get an extra empty token in the middle.
    549                                 if ("width".equals(tokens[0])) {
    550                                     mSize1 = Integer.parseInt(tokens[tokens.length-1]);
    551                                 } else if ("height".equals(tokens[0])) {
    552                                     mSize2 = Integer.parseInt(tokens[tokens.length-1]);
    553                                 }
    554                             }
    555                         }
    556 
    557                         return mSize1 != -1 && mSize2 != -1;
    558                     }
    559                 }
    560 
    561             }
    562             // if it reaches here, display was not found.
    563             // false is returned below.
    564         } catch (IOException e) {
    565             // ignore.
    566         }
    567 
    568         return false;
    569     }
    570 
    571     /**
    572      * @return Whether there's a snapshot file available.
    573      */
    574     public boolean hasSnapshot() {
    575         return mHasSnapshot;
    576     }
    577 
    578     /**
    579      * @return Whether to launch and load snapshot.
    580      */
    581     public boolean hasSnapshotLaunch() {
    582         return mSnapshotLaunch && !hasWipeData();
    583     }
    584 
    585     /**
    586      * @return Whether to preserve emulator state to snapshot.
    587      */
    588     public boolean hasSnapshotSave() {
    589         return mSnapshotSave;
    590     }
    591 
    592     /**
    593      * Updates snapshot launch availability, for when mWipeData value changes.
    594      */
    595     private void updateSnapshotLaunchAvailability() {
    596         boolean enabled = !mWipeData && mHasSnapshot;
    597         mSnapshotLaunchCheckbox.setEnabled(enabled);
    598         mSnapshotLaunch = enabled && sSnapshotLaunch;
    599         mSnapshotLaunchCheckbox.setSelection(mSnapshotLaunch);
    600     }
    601 
    602 }
    603