• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation, SettingsController settingsController)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 
hasWipeData()109     public boolean hasWipeData() {
110         return mWipeData;
111     }
112 
113     /**
114      * Returns the scaling factor, or 0.f if none are set.
115      */
getScale()116     public float getScale() {
117         return mScale;
118     }
119 
120     @Override
createDialogContent(final Composite parent)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 */
enableGroup(final Group group, boolean enabled)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
configureShell(Shell newShell)303     protected void configureShell(Shell newShell) {
304         super.configureShell(newShell);
305         newShell.setText("Launch Options");
306     }
307 
308     @Override
createButton(Composite parent, int id, String label, boolean defaultButton)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
okPressed()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 
computeSkinData()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 
onScaleChange()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 
setScale(float scale)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      */
getMonitorDpi()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      */
getScreenSize()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      */
getDensityText()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      */
findSkinResolution()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      */
parseLayoutFile(File layoutFile)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      */
hasSnapshot()574     public boolean hasSnapshot() {
575         return mHasSnapshot;
576     }
577 
578     /**
579      * @return Whether to launch and load snapshot.
580      */
hasSnapshotLaunch()581     public boolean hasSnapshotLaunch() {
582         return mSnapshotLaunch && !hasWipeData();
583     }
584 
585     /**
586      * @return Whether to preserve emulator state to snapshot.
587      */
hasSnapshotSave()588     public boolean hasSnapshotSave() {
589         return mSnapshotSave;
590     }
591 
592     /**
593      * Updates snapshot launch availability, for when mWipeData value changes.
594      */
updateSnapshotLaunchAvailability()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