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 public void verifyText(VerifyEvent event) { 158 // combine the current content and the new text 159 String text = mScreenSize.getText(); 160 text = text.substring(0, event.start) + event.text + text.substring(event.end); 161 162 // now make sure it's a match for the regex 163 event.doit = sScreenSizePattern.matcher(text).matches(); 164 } 165 }); 166 mScreenSize.addModifyListener(new ModifyListener() { 167 public void modifyText(ModifyEvent event) { 168 onScaleChange(); 169 } 170 }); 171 172 // empty composite, only 2 widgets on this line. 173 new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData()); 174 gd.widthHint = gd.heightHint = 0; 175 176 l = new Label(scaleGroup, SWT.NONE); 177 l.setText("Monitor dpi:"); 178 mMonitorDpi = new Text(scaleGroup, SWT.BORDER); 179 mMonitorDpi.setText(Integer.toString(getMonitorDpi())); 180 mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 181 gd.widthHint = 50; 182 mMonitorDpi.addVerifyListener(new VerifyListener() { 183 public void verifyText(VerifyEvent event) { 184 // check for digit only. 185 for (int i = 0 ; i < event.text.length(); i++) { 186 char letter = event.text.charAt(i); 187 if (letter < '0' || letter > '9') { 188 event.doit = false; 189 return; 190 } 191 } 192 } 193 }); 194 mMonitorDpi.addModifyListener(new ModifyListener() { 195 public void modifyText(ModifyEvent event) { 196 onScaleChange(); 197 } 198 }); 199 200 Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT); 201 button.setText("?"); 202 button.setToolTipText("Click to figure out your monitor's pixel density"); 203 button.addSelectionListener(new SelectionAdapter() { 204 @Override 205 public void widgetSelected(SelectionEvent arg0) { 206 ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell()); 207 if (dialog.open() == Window.OK) { 208 mMonitorDpi.setText(Integer.toString(dialog.getDensity())); 209 } 210 } 211 }); 212 213 l = new Label(scaleGroup, SWT.NONE); 214 l.setText("Scale:"); 215 mScaleField = new Label(scaleGroup, SWT.NONE); 216 mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, 217 true /*grabExcessHorizontalSpace*/, 218 true /*grabExcessVerticalSpace*/, 219 2 /*horizontalSpan*/, 220 1 /*verticalSpan*/)); 221 setScale(mScale); // set initial text value 222 223 enableGroup(scaleGroup, defaultState); 224 225 mScaleButton.addSelectionListener(new SelectionAdapter() { 226 @Override 227 public void widgetSelected(SelectionEvent event) { 228 boolean enabled = mScaleButton.getSelection(); 229 enableGroup(scaleGroup, enabled); 230 if (enabled) { 231 onScaleChange(); 232 } else { 233 setScale(0); 234 } 235 } 236 }); 237 238 final Button wipeButton = new Button(parent, SWT.CHECK); 239 wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 240 gd.horizontalSpan = 2; 241 wipeButton.setText("Wipe user data"); 242 wipeButton.setSelection(mWipeData = sWipeData); 243 wipeButton.addSelectionListener(new SelectionAdapter() { 244 @Override 245 public void widgetSelected(SelectionEvent arg0) { 246 mWipeData = wipeButton.getSelection(); 247 updateSnapshotLaunchAvailability(); 248 } 249 }); 250 251 Map<String, String> prop = mAvd.getProperties(); 252 String snapshotPresent = prop.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); 253 mHasSnapshot = (snapshotPresent != null) && snapshotPresent.equals("true"); 254 255 mSnapshotLaunchCheckbox = new Button(parent, SWT.CHECK); 256 mSnapshotLaunchCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 257 gd.horizontalSpan = 2; 258 mSnapshotLaunchCheckbox.setText("Launch from snapshot"); 259 updateSnapshotLaunchAvailability(); 260 mSnapshotLaunchCheckbox.addSelectionListener(new SelectionAdapter() { 261 @Override 262 public void widgetSelected(SelectionEvent arg0) { 263 mSnapshotLaunch = mSnapshotLaunchCheckbox.getSelection(); 264 } 265 }); 266 267 final Button snapshotSaveCheckbox = new Button(parent, SWT.CHECK); 268 snapshotSaveCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 269 gd.horizontalSpan = 2; 270 snapshotSaveCheckbox.setText("Save to snapshot"); 271 snapshotSaveCheckbox.setSelection((mSnapshotSave = sSnapshotSave) && mHasSnapshot); 272 snapshotSaveCheckbox.setEnabled(mHasSnapshot); 273 snapshotSaveCheckbox.addSelectionListener(new SelectionAdapter() { 274 @Override 275 public void widgetSelected(SelectionEvent arg0) { 276 mSnapshotSave = snapshotSaveCheckbox.getSelection(); 277 } 278 }); 279 280 l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); 281 l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 282 gd.horizontalSpan = 2; 283 284 // if the scaling is enabled by default, we must initialize the value of mScale 285 if (defaultState) { 286 onScaleChange(); 287 } 288 } 289 290 /** On Windows we need to manually enable/disable the children of a group */ 291 private void enableGroup(final Group group, boolean enabled) { 292 group.setEnabled(enabled); 293 for (Control c : group.getChildren()) { 294 c.setEnabled(enabled); 295 } 296 } 297 298 @Override 299 protected void configureShell(Shell newShell) { 300 super.configureShell(newShell); 301 newShell.setText("Launch Options"); 302 } 303 304 @Override 305 protected Button createButton(Composite parent, int id, String label, boolean defaultButton) { 306 if (id == IDialogConstants.OK_ID) { 307 label = "Launch"; 308 } 309 310 return super.createButton(parent, id, label, defaultButton); 311 } 312 313 @Override 314 protected void okPressed() { 315 // override ok to store some info 316 // first the monitor dpi 317 String dpi = mMonitorDpi.getText(); 318 if (dpi.length() > 0) { 319 sMonitorDpi = Integer.parseInt(dpi); 320 321 // if there is a setting controller, save it 322 if (mSettingsController != null) { 323 mSettingsController.setMonitorDensity(sMonitorDpi); 324 mSettingsController.saveSettings(); 325 } 326 } 327 328 // now the scale factor 329 String key = mAvd.getName(); 330 sSkinScaling.remove(key); 331 if (mScaleButton.getSelection()) { 332 String size = mScreenSize.getText(); 333 if (size.length() > 0) { 334 sSkinScaling.put(key, size); 335 } 336 } 337 338 // and then the wipe-data checkbox 339 sWipeData = mWipeData; 340 341 // and the snapshot handling if those checkboxes are enabled. 342 if (mHasSnapshot) { 343 sSnapshotSave = mSnapshotSave; 344 if (!mWipeData) { 345 sSnapshotLaunch = mSnapshotLaunch; 346 } 347 } 348 349 // finally continue with the ok action 350 super.okPressed(); 351 } 352 353 private void computeSkinData() { 354 Map<String, String> prop = mAvd.getProperties(); 355 String dpi = prop.get("hw.lcd.density"); 356 if (dpi != null && dpi.length() > 0) { 357 mDensity = Integer.parseInt(dpi); 358 } 359 360 findSkinResolution(); 361 } 362 363 private void onScaleChange() { 364 String sizeStr = mScreenSize.getText(); 365 if (sizeStr.length() == 0) { 366 setScale(0); 367 return; 368 } 369 370 String dpiStr = mMonitorDpi.getText(); 371 if (dpiStr.length() == 0) { 372 setScale(0); 373 return; 374 } 375 376 int dpi = Integer.parseInt(dpiStr); 377 float size = Float.parseFloat(sizeStr); 378 /* 379 * We are trying to emulate the following device: 380 * resolution: 'mSize1'x'mSize2' 381 * density: 'mDensity' 382 * screen diagonal: 'size' 383 * ontop a monitor running at 'dpi' 384 */ 385 // We start by computing the screen diagonal in pixels, if the density was really mDensity 386 float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2); 387 // Now we would convert this in actual inches: 388 // diagonalIn = diagonal / mDensity 389 // the scale factor is a mix of adapting to the new density and to the new size. 390 // (size/diagonalIn) * (dpi/mDensity) 391 // this can be simplified to: 392 setScale((size * dpi) / diagonalPx); 393 } 394 395 private void setScale(float scale) { 396 mScale = scale; 397 398 // Do the rounding exactly like AvdSelector will do. 399 scale = Math.round(scale * 100); 400 scale /= 100.f; 401 402 if (scale == 0.f) { 403 mScaleField.setText("default"); //$NON-NLS-1$ 404 } else { 405 mScaleField.setText(String.format("%.2f", scale)); //$NON-NLS-1$ 406 } 407 } 408 409 /** 410 * Returns the monitor dpi to start with. 411 * This can be coming from the settings, the session-based storage, or the from whatever Java 412 * can tell us. 413 */ 414 private int getMonitorDpi() { 415 if (mSettingsController != null) { 416 sMonitorDpi = mSettingsController.getMonitorDensity(); 417 } 418 419 if (sMonitorDpi == -1) { // first time? try to get a value 420 sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution(); 421 } 422 423 return sMonitorDpi; 424 } 425 426 /** 427 * Returns the screen size to start with. 428 * <p/>If an emulator with the same skin was already launched, scaled, the size used is reused. 429 * <p/>Otherwise the default is returned (3) 430 */ 431 private String getScreenSize() { 432 String size = sSkinScaling.get(mAvd.getName()); 433 if (size != null) { 434 return size; 435 } 436 437 return "3"; 438 } 439 440 /** 441 * Returns a display string for the density. 442 */ 443 private String getDensityText() { 444 switch (mDensity) { 445 case 120: 446 return "Low (120)"; 447 case 160: 448 return "Medium (160)"; 449 case 240: 450 return "High (240)"; 451 } 452 453 return Integer.toString(mDensity); 454 } 455 456 /** 457 * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}. 458 */ 459 private void findSkinResolution() { 460 Map<String, String> prop = mAvd.getProperties(); 461 String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME); 462 463 if (skinName != null) { 464 Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName); 465 if (m != null && m.matches()) { 466 mSize1 = Integer.parseInt(m.group(1)); 467 mSize2 = Integer.parseInt(m.group(2)); 468 mSkinDisplay = skinName; 469 mEnableScaling = true; 470 return; 471 } 472 } 473 474 // The resolution is inside the layout file of the skin. 475 mEnableScaling = false; // default to false for now. 476 477 // path to the skin layout file. 478 String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH); 479 if (skinPath != null) { 480 File skinFolder = new File(mSdkLocation, skinPath); 481 if (skinFolder.isDirectory()) { 482 File layoutFile = new File(skinFolder, "layout"); 483 if (layoutFile.isFile()) { 484 if (parseLayoutFile(layoutFile)) { 485 mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2); 486 mEnableScaling = true; 487 } else { 488 mSkinDisplay = skinName; 489 } 490 } 491 } 492 } 493 } 494 495 /** 496 * Parses a layout file. 497 * <p/> 498 * the format is relatively easy. It's a collection of items defined as 499 * ≶name> { 500 * ≶content> 501 * } 502 * 503 * content is either 1+ items or 1+ properties 504 * properties are defined as 505 * ≶name>≶whitespace>≶value> 506 * 507 * We're going to look for an item called display, with 2 properties height and width. 508 * This is very basic parser. 509 * 510 * @param layoutFile the file to parse 511 * @return true if both sizes where found. 512 */ 513 private boolean parseLayoutFile(File layoutFile) { 514 try { 515 BufferedReader input = new BufferedReader(new FileReader(layoutFile)); 516 String line; 517 518 while ((line = input.readLine()) != null) { 519 // trim to remove whitespace 520 line = line.trim(); 521 int len = line.length(); 522 if (len == 0) continue; 523 524 // check if this is a new item 525 if (line.charAt(len-1) == '{') { 526 // this is the start of a node 527 String[] tokens = line.split(" "); 528 if ("display".equals(tokens[0])) { 529 // this is the one we're looking for! 530 while ((mSize1 == -1 || mSize2 == -1) && 531 (line = input.readLine()) != null) { 532 // trim to remove whitespace 533 line = line.trim(); 534 len = line.length(); 535 if (len == 0) continue; 536 537 if ("}".equals(line)) { // looks like we're done with the item. 538 break; 539 } 540 541 tokens = line.split(" "); 542 if (tokens.length >= 2) { 543 // there can be multiple space between the name and value 544 // in which case we'll get an extra empty token in the middle. 545 if ("width".equals(tokens[0])) { 546 mSize1 = Integer.parseInt(tokens[tokens.length-1]); 547 } else if ("height".equals(tokens[0])) { 548 mSize2 = Integer.parseInt(tokens[tokens.length-1]); 549 } 550 } 551 } 552 553 return mSize1 != -1 && mSize2 != -1; 554 } 555 } 556 557 } 558 // if it reaches here, display was not found. 559 // false is returned below. 560 } catch (IOException e) { 561 // ignore. 562 } 563 564 return false; 565 } 566 567 /** 568 * @return Whether there's a snapshot file available. 569 */ 570 public boolean hasSnapshot() { 571 return mHasSnapshot; 572 } 573 574 /** 575 * @return Whether to launch and load snapshot. 576 */ 577 public boolean hasSnapshotLaunch() { 578 return mSnapshotLaunch && !hasWipeData(); 579 } 580 581 /** 582 * @return Whether to preserve emulator state to snapshot. 583 */ 584 public boolean hasSnapshotSave() { 585 return mSnapshotSave; 586 } 587 588 /** 589 * Updates snapshot launch availability, for when mWipeData value changes. 590 */ 591 private void updateSnapshotLaunchAvailability() { 592 boolean enabled = !mWipeData && mHasSnapshot; 593 mSnapshotLaunchCheckbox.setEnabled(enabled); 594 mSnapshotLaunch = enabled && sSnapshotLaunch; 595 mSnapshotLaunchCheckbox.setSelection(mSnapshotLaunch); 596 } 597 598 } 599