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.AvdManager; 20 import com.android.sdklib.internal.avd.AvdManager.AvdInfo; 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 * 60 * Values are stored (in the class as static field) to be reused while the app is still running. 61 * The Monitor dpi is stored in the settings if availabe. 62 */ 63 final class AvdStartDialog extends GridDialog { 64 // static field to reuse values during the same session. 65 private static boolean sWipeData = false; 66 private static int sMonitorDpi = 72; // used if there's no setting controller. 67 private static final Map<String, String> sSkinScaling = new HashMap<String, String>(); 68 69 private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?"); 70 71 private final AvdInfo mAvd; 72 private final String mSdkLocation; 73 private final SettingsController mSettingsController; 74 75 private Text mScreenSize; 76 private Text mMonitorDpi; 77 private Button mScaleButton; 78 79 private float mScale = 0.f; 80 private boolean mWipeData = false; 81 private int mDensity = 160; // medium density 82 private int mSize1 = -1; 83 private int mSize2 = -1; 84 private String mSkinDisplay; 85 private boolean mEnableScaling = true; 86 private Label mScaleField; 87 AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation, SettingsController settingsController)88 AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation, 89 SettingsController settingsController) { 90 super(parentShell, 2, false); 91 mAvd = avd; 92 mSdkLocation = sdkLocation; 93 mSettingsController = settingsController; 94 if (mAvd == null) { 95 throw new IllegalArgumentException("avd cannot be null"); 96 } 97 if (mSdkLocation == null) { 98 throw new IllegalArgumentException("sdkLocation cannot be null"); 99 } 100 101 computeSkinData(); 102 } 103 getWipeData()104 public boolean getWipeData() { 105 return mWipeData; 106 } 107 108 /** 109 * Returns the scaling factor, or 0.f if none are set. 110 */ getScale()111 public float getScale() { 112 return mScale; 113 } 114 115 @Override createDialogContent(final Composite parent)116 public void createDialogContent(final Composite parent) { 117 GridData gd; 118 119 Label l = new Label(parent, SWT.NONE); 120 l.setText("Skin:"); 121 122 l = new Label(parent, SWT.NONE); 123 l.setText(mSkinDisplay == null ? "None" : mSkinDisplay); 124 l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 125 126 l = new Label(parent, SWT.NONE); 127 l.setText("Density:"); 128 129 l = new Label(parent, SWT.NONE); 130 l.setText(getDensityText()); 131 l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 132 133 mScaleButton = new Button(parent, SWT.CHECK); 134 mScaleButton.setText("Scale display to real size"); 135 mScaleButton.setEnabled(mEnableScaling); 136 boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null; 137 mScaleButton.setSelection(defaultState); 138 mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 139 gd.horizontalSpan = 2; 140 final Group scaleGroup = new Group(parent, SWT.NONE); 141 scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 142 gd.horizontalIndent = 30; 143 gd.horizontalSpan = 2; 144 scaleGroup.setLayout(new GridLayout(3, false)); 145 146 l = new Label(scaleGroup, SWT.NONE); 147 l.setText("Screen Size (in):"); 148 mScreenSize = new Text(scaleGroup, SWT.BORDER); 149 mScreenSize.setText(getScreenSize()); 150 mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 151 mScreenSize.addVerifyListener(new VerifyListener() { 152 public void verifyText(VerifyEvent event) { 153 // combine the current content and the new text 154 String text = mScreenSize.getText(); 155 text = text.substring(0, event.start) + event.text + text.substring(event.end); 156 157 // now make sure it's a match for the regex 158 event.doit = sScreenSizePattern.matcher(text).matches(); 159 } 160 }); 161 mScreenSize.addModifyListener(new ModifyListener() { 162 public void modifyText(ModifyEvent event) { 163 onScaleChange(); 164 } 165 }); 166 167 // empty composite, only 2 widgets on this line. 168 new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData()); 169 gd.widthHint = gd.heightHint = 0; 170 171 l = new Label(scaleGroup, SWT.NONE); 172 l.setText("Monitor dpi:"); 173 mMonitorDpi = new Text(scaleGroup, SWT.BORDER); 174 mMonitorDpi.setText(Integer.toString(getMonitorDpi())); 175 mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 176 gd.widthHint = 50; 177 mMonitorDpi.addVerifyListener(new VerifyListener() { 178 public void verifyText(VerifyEvent event) { 179 // check for digit only. 180 for (int i = 0 ; i < event.text.length(); i++) { 181 char letter = event.text.charAt(i); 182 if (letter < '0' || letter > '9') { 183 event.doit = false; 184 return; 185 } 186 } 187 } 188 }); 189 mMonitorDpi.addModifyListener(new ModifyListener() { 190 public void modifyText(ModifyEvent event) { 191 onScaleChange(); 192 } 193 }); 194 195 Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT); 196 button.setText("?"); 197 button.setToolTipText("Click to figure out your monitor's pixel density"); 198 button.addSelectionListener(new SelectionAdapter() { 199 @Override 200 public void widgetSelected(SelectionEvent arg0) { 201 ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell()); 202 if (dialog.open() == Window.OK) { 203 mMonitorDpi.setText(Integer.toString(dialog.getDensity())); 204 } 205 } 206 }); 207 208 l = new Label(scaleGroup, SWT.NONE); 209 l.setText("Scale:"); 210 mScaleField = new Label(scaleGroup, SWT.NONE); 211 mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, 212 true /*grabExcessHorizontalSpace*/, 213 true /*grabExcessVerticalSpace*/, 214 2 /*horizontalSpan*/, 215 1 /*verticalSpan*/)); 216 setScale(mScale); // set initial text value 217 218 enableGroup(scaleGroup, defaultState); 219 220 mScaleButton.addSelectionListener(new SelectionAdapter() { 221 @Override 222 public void widgetSelected(SelectionEvent event) { 223 boolean enabled = mScaleButton.getSelection(); 224 enableGroup(scaleGroup, enabled); 225 if (enabled) { 226 onScaleChange(); 227 } else { 228 setScale(0); 229 } 230 } 231 }); 232 233 final Button wipeButton = new Button(parent, SWT.CHECK); 234 wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 235 gd.horizontalSpan = 2; 236 wipeButton.setText("Wipe user data"); 237 wipeButton.setSelection(mWipeData = sWipeData); 238 wipeButton.addSelectionListener(new SelectionAdapter() { 239 @Override 240 public void widgetSelected(SelectionEvent arg0) { 241 mWipeData = wipeButton.getSelection(); 242 } 243 }); 244 245 l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); 246 l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); 247 gd.horizontalSpan = 2; 248 249 // if the scaling is enabled by default, we must initialize the value of mScale 250 if (defaultState) { 251 onScaleChange(); 252 } 253 } 254 255 /** On Windows we need to manually enable/disable the children of a group */ enableGroup(final Group group, boolean enabled)256 private void enableGroup(final Group group, boolean enabled) { 257 group.setEnabled(enabled); 258 for (Control c : group.getChildren()) { 259 c.setEnabled(enabled); 260 } 261 } 262 263 @Override configureShell(Shell newShell)264 protected void configureShell(Shell newShell) { 265 super.configureShell(newShell); 266 newShell.setText("Launch Options"); 267 } 268 269 @Override createButton(Composite parent, int id, String label, boolean defaultButton)270 protected Button createButton(Composite parent, int id, String label, boolean defaultButton) { 271 if (id == IDialogConstants.OK_ID) { 272 label = "Launch"; 273 } 274 275 return super.createButton(parent, id, label, defaultButton); 276 } 277 278 @Override okPressed()279 protected void okPressed() { 280 // override ok to store some info 281 // first the monitor dpi 282 String dpi = mMonitorDpi.getText(); 283 if (dpi.length() > 0) { 284 sMonitorDpi = Integer.parseInt(dpi); 285 286 // if there is a setting controller, save it 287 if (mSettingsController != null) { 288 mSettingsController.setMonitorDensity(sMonitorDpi); 289 mSettingsController.saveSettings(); 290 } 291 } 292 293 // now the scale factor 294 String key = mAvd.getName(); 295 sSkinScaling.remove(key); 296 if (mScaleButton.getSelection()) { 297 String size = mScreenSize.getText(); 298 if (size.length() > 0) { 299 sSkinScaling.put(key, size); 300 } 301 } 302 303 // and then the wipe-data checkbox 304 sWipeData = mWipeData; 305 306 // finally continue with the ok action 307 super.okPressed(); 308 } 309 computeSkinData()310 private void computeSkinData() { 311 Map<String, String> prop = mAvd.getProperties(); 312 String dpi = prop.get("hw.lcd.density"); 313 if (dpi != null && dpi.length() > 0) { 314 mDensity = Integer.parseInt(dpi); 315 } 316 317 findSkinResolution(); 318 } 319 onScaleChange()320 private void onScaleChange() { 321 String sizeStr = mScreenSize.getText(); 322 if (sizeStr.length() == 0) { 323 setScale(0); 324 return; 325 } 326 327 String dpiStr = mMonitorDpi.getText(); 328 if (dpiStr.length() == 0) { 329 setScale(0); 330 return; 331 } 332 333 int dpi = Integer.parseInt(dpiStr); 334 float size = Float.parseFloat(sizeStr); 335 /* 336 * We are trying to emulate the following device: 337 * resolution: 'mSize1'x'mSize2' 338 * density: 'mDensity' 339 * screen diagonal: 'size' 340 * ontop a monitor running at 'dpi' 341 */ 342 // We start by computing the screen diagonal in pixels, if the density was really mDensity 343 float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2); 344 // Now we would convert this in actual inches: 345 // diagonalIn = diagonal / mDensity 346 // the scale factor is a mix of adapting to the new density and to the new size. 347 // (size/diagonalIn) * (dpi/mDensity) 348 // this can be simplified to: 349 setScale((size * dpi) / diagonalPx); 350 } 351 setScale(float scale)352 private void setScale(float scale) { 353 mScale = scale; 354 355 // Do the rounding exactly like AvdSelector will do. 356 scale = Math.round(scale * 100); 357 scale /= 100.f; 358 359 if (scale == 0.f) { 360 mScaleField.setText("default"); //$NON-NLS-1$ 361 } else { 362 mScaleField.setText(String.format("%.2f", scale)); //$NON-NLS-1$ 363 } 364 } 365 366 /** 367 * Returns the monitor dpi to start with. 368 * This can be coming from the settings, the session-based storage, or the from whatever Java 369 * can tell us. 370 */ getMonitorDpi()371 private int getMonitorDpi() { 372 if (mSettingsController != null) { 373 sMonitorDpi = mSettingsController.getMonitorDensity(); 374 } 375 376 if (sMonitorDpi == -1) { // first time? try to get a value 377 sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution(); 378 } 379 380 return sMonitorDpi; 381 } 382 383 /** 384 * Returns the screen size to start with. 385 * <p/>If an emulator with the same skin was already launched, scaled, the size used is reused. 386 * <p/>Otherwise the default is returned (3) 387 */ getScreenSize()388 private String getScreenSize() { 389 String size = sSkinScaling.get(mAvd.getName()); 390 if (size != null) { 391 return size; 392 } 393 394 return "3"; 395 } 396 397 /** 398 * Returns a display string for the density. 399 */ getDensityText()400 private String getDensityText() { 401 switch (mDensity) { 402 case 120: 403 return "Low (120)"; 404 case 160: 405 return "Medium (160)"; 406 case 240: 407 return "High (240)"; 408 } 409 410 return Integer.toString(mDensity); 411 } 412 413 /** 414 * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}. 415 */ findSkinResolution()416 private void findSkinResolution() { 417 Map<String, String> prop = mAvd.getProperties(); 418 String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME); 419 420 if (skinName != null) { 421 Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName); 422 if (m != null && m.matches()) { 423 mSize1 = Integer.parseInt(m.group(1)); 424 mSize2 = Integer.parseInt(m.group(2)); 425 mSkinDisplay = skinName; 426 mEnableScaling = true; 427 return; 428 } 429 } 430 431 // The resolution is inside the layout file of the skin. 432 mEnableScaling = false; // default to false for now. 433 434 // path to the skin layout file. 435 String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH); 436 if (skinPath != null) { 437 File skinFolder = new File(mSdkLocation, skinPath); 438 if (skinFolder.isDirectory()) { 439 File layoutFile = new File(skinFolder, "layout"); 440 if (layoutFile.isFile()) { 441 if (parseLayoutFile(layoutFile)) { 442 mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2); 443 mEnableScaling = true; 444 } else { 445 mSkinDisplay = skinName; 446 } 447 } 448 } 449 } 450 } 451 452 /** 453 * Parses a layout file. 454 * <p/> 455 * the format is relatively easy. It's a collection of items defined as 456 * ≶name> { 457 * ≶content> 458 * } 459 * 460 * content is either 1+ items or 1+ properties 461 * properties are defined as 462 * ≶name>≶whitespace>≶value> 463 * 464 * We're going to look for an item called display, with 2 properties height and width. 465 * This is very basic parser. 466 * 467 * @param layoutFile the file to parse 468 * @return true if both sizes where found. 469 */ parseLayoutFile(File layoutFile)470 private boolean parseLayoutFile(File layoutFile) { 471 try { 472 BufferedReader input = new BufferedReader(new FileReader(layoutFile)); 473 String line; 474 475 while ((line = input.readLine()) != null) { 476 // trim to remove whitespace 477 line = line.trim(); 478 int len = line.length(); 479 if (len == 0) continue; 480 481 // check if this is a new item 482 if (line.charAt(len-1) == '{') { 483 // this is the start of a node 484 String[] tokens = line.split(" "); 485 if ("display".equals(tokens[0])) { 486 // this is the one we're looking for! 487 while ((mSize1 == -1 || mSize2 == -1) && 488 (line = input.readLine()) != null) { 489 // trim to remove whitespace 490 line = line.trim(); 491 len = line.length(); 492 if (len == 0) continue; 493 494 if ("}".equals(line)) { // looks like we're done with the item. 495 break; 496 } 497 498 tokens = line.split(" "); 499 if (tokens.length >= 2) { 500 // there can be multiple space between the name and value 501 // in which case we'll get an extra empty token in the middle. 502 if ("width".equals(tokens[0])) { 503 mSize1 = Integer.parseInt(tokens[tokens.length-1]); 504 } else if ("height".equals(tokens[0])) { 505 mSize2 = Integer.parseInt(tokens[tokens.length-1]); 506 } 507 } 508 } 509 510 return mSize1 != -1 && mSize2 != -1; 511 } 512 } 513 514 } 515 // if it reaches here, display was not found. 516 // false is returned below. 517 } catch (IOException e) { 518 // ignore. 519 } 520 521 return false; 522 } 523 } 524