1 /* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java 2 ** 3 ** Copyright 2007, The Android Open Source Project 4 ** 5 ** Licensed under the Apache License, Version 2.0 (the "License"); 6 ** you may not use this file except in compliance with the License. 7 ** You may obtain a copy of the License at 8 ** 9 ** http://www.apache.org/licenses/LICENSE-2.0 10 ** 11 ** Unless required by applicable law or agreed to in writing, software 12 ** distributed under the License is distributed on an "AS IS" BASIS, 13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 ** See the License for the specific language governing permissions and 15 ** limitations under the License. 16 */ 17 18 package com.android.ddms; 19 20 import com.android.ddmlib.IDevice; 21 import com.android.ddmlib.IShellOutputReceiver; 22 import com.android.ddmlib.Log; 23 24 import org.eclipse.swt.SWT; 25 import org.eclipse.swt.events.SelectionAdapter; 26 import org.eclipse.swt.events.SelectionEvent; 27 import org.eclipse.swt.graphics.Font; 28 import org.eclipse.swt.graphics.FontData; 29 import org.eclipse.swt.layout.GridData; 30 import org.eclipse.swt.layout.GridLayout; 31 import org.eclipse.swt.widgets.Button; 32 import org.eclipse.swt.widgets.Dialog; 33 import org.eclipse.swt.widgets.Display; 34 import org.eclipse.swt.widgets.Event; 35 import org.eclipse.swt.widgets.FileDialog; 36 import org.eclipse.swt.widgets.Label; 37 import org.eclipse.swt.widgets.Listener; 38 import org.eclipse.swt.widgets.Shell; 39 import org.eclipse.swt.widgets.Text; 40 41 import java.io.BufferedOutputStream; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.UnsupportedEncodingException; 45 46 47 /** 48 * Execute a command on an ADB-attached device and save the output. 49 * 50 * There are several ways to do this. One is to run a single command 51 * and show the output. Another is to have several possible commands and 52 * let the user click a button next to the one (or ones) they want. This 53 * currently uses the simple 1:1 form. 54 */ 55 public class DeviceCommandDialog extends Dialog { 56 57 public static final int DEVICE_STATE = 0; 58 public static final int APP_STATE = 1; 59 public static final int RADIO_STATE = 2; 60 public static final int LOGCAT = 3; 61 62 private String mCommand; 63 private String mFileName; 64 65 private Label mStatusLabel; 66 private Button mCancelDone; 67 private Button mSave; 68 private Text mText; 69 private Font mFont = null; 70 private boolean mCancel; 71 private boolean mFinished; 72 73 74 /** 75 * Create with default style. 76 */ 77 public DeviceCommandDialog(String command, String fileName, Shell parent) { 78 // don't want a close button, but it seems hard to get rid of on GTK 79 // keep it on all platforms for consistency 80 this(command, fileName, parent, 81 SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE); 82 } 83 84 /** 85 * Create with app-defined style. 86 */ 87 public DeviceCommandDialog(String command, String fileName, Shell parent, 88 int style) 89 { 90 super(parent, style); 91 mCommand = command; 92 mFileName = fileName; 93 } 94 95 /** 96 * Prepare and display the dialog. 97 * @param currentDevice 98 */ 99 public void open(IDevice currentDevice) { 100 Shell parent = getParent(); 101 Shell shell = new Shell(parent, getStyle()); 102 shell.setText("Remote Command"); 103 104 mFinished = false; 105 mFont = findFont(shell.getDisplay()); 106 createContents(shell); 107 108 // Getting weird layout behavior under Linux when Text is added -- 109 // looks like text widget has min width of 400 when FILL_HORIZONTAL 110 // is used, and layout gets tweaked to force this. (Might be even 111 // more with the scroll bars in place -- it wigged out when the 112 // file save dialog was invoked.) 113 shell.setMinimumSize(500, 200); 114 shell.setSize(800, 600); 115 shell.open(); 116 117 executeCommand(shell, currentDevice); 118 119 Display display = parent.getDisplay(); 120 while (!shell.isDisposed()) { 121 if (!display.readAndDispatch()) 122 display.sleep(); 123 } 124 125 if (mFont != null) 126 mFont.dispose(); 127 } 128 129 /* 130 * Create a text widget to show the output and some buttons to 131 * manage things. 132 */ 133 private void createContents(final Shell shell) { 134 GridData data; 135 136 shell.setLayout(new GridLayout(2, true)); 137 138 shell.addListener(SWT.Close, new Listener() { 139 public void handleEvent(Event event) { 140 if (!mFinished) { 141 Log.d("ddms", "NOT closing - cancelling command"); 142 event.doit = false; 143 mCancel = true; 144 } 145 } 146 }); 147 148 mStatusLabel = new Label(shell, SWT.NONE); 149 mStatusLabel.setText("Executing '" + shortCommandString() + "'"); 150 data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); 151 data.horizontalSpan = 2; 152 mStatusLabel.setLayoutData(data); 153 154 mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); 155 mText.setEditable(false); 156 mText.setFont(mFont); 157 data = new GridData(GridData.FILL_BOTH); 158 data.horizontalSpan = 2; 159 mText.setLayoutData(data); 160 161 // "save" button 162 mSave = new Button(shell, SWT.PUSH); 163 mSave.setText("Save"); 164 data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); 165 data.widthHint = 80; 166 mSave.setLayoutData(data); 167 mSave.addSelectionListener(new SelectionAdapter() { 168 @Override 169 public void widgetSelected(SelectionEvent e) { 170 saveText(shell); 171 } 172 }); 173 mSave.setEnabled(false); 174 175 // "cancel/done" button 176 mCancelDone = new Button(shell, SWT.PUSH); 177 mCancelDone.setText("Cancel"); 178 data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); 179 data.widthHint = 80; 180 mCancelDone.setLayoutData(data); 181 mCancelDone.addSelectionListener(new SelectionAdapter() { 182 @Override 183 public void widgetSelected(SelectionEvent e) { 184 if (!mFinished) 185 mCancel = true; 186 else 187 shell.close(); 188 } 189 }); 190 } 191 192 /* 193 * Figure out what font to use. 194 * 195 * Returns "null" if we can't figure it out, which SWT understands to 196 * mean "use default system font". 197 */ 198 private Font findFont(Display display) { 199 String fontStr = PrefsDialog.getStore().getString("textOutputFont"); 200 if (fontStr != null) { 201 FontData fdat = new FontData(fontStr); 202 if (fdat != null) 203 return new Font(display, fdat); 204 } 205 return null; 206 } 207 208 209 /* 210 * Callback class for command execution. 211 */ 212 class Gatherer extends Thread implements IShellOutputReceiver { 213 public static final int RESULT_UNKNOWN = 0; 214 public static final int RESULT_SUCCESS = 1; 215 public static final int RESULT_FAILURE = 2; 216 public static final int RESULT_CANCELLED = 3; 217 218 private Shell mShell; 219 private String mCommand; 220 private Text mText; 221 private int mResult; 222 private IDevice mDevice; 223 224 /** 225 * Constructor; pass in the text widget that will receive the output. 226 * @param device 227 */ 228 public Gatherer(Shell shell, IDevice device, String command, Text text) { 229 mShell = shell; 230 mDevice = device; 231 mCommand = command; 232 mText = text; 233 mResult = RESULT_UNKNOWN; 234 235 // this is in outer class 236 mCancel = false; 237 } 238 239 /** 240 * Thread entry point. 241 */ 242 @Override 243 public void run() { 244 245 if (mDevice == null) { 246 Log.w("ddms", "Cannot execute command: no device selected."); 247 mResult = RESULT_FAILURE; 248 } else { 249 try { 250 mDevice.executeShellCommand(mCommand, this); 251 if (mCancel) 252 mResult = RESULT_CANCELLED; 253 else 254 mResult = RESULT_SUCCESS; 255 } 256 catch (IOException ioe) { 257 Log.w("ddms", "Remote exec failed: " + ioe.getMessage()); 258 mResult = RESULT_FAILURE; 259 } 260 } 261 262 mShell.getDisplay().asyncExec(new Runnable() { 263 public void run() { 264 updateForResult(mResult); 265 } 266 }); 267 } 268 269 /** 270 * Called by executeRemoteCommand(). 271 */ 272 public void addOutput(byte[] data, int offset, int length) { 273 274 Log.v("ddms", "received " + length + " bytes"); 275 try { 276 final String text; 277 text = new String(data, offset, length, "ISO-8859-1"); 278 279 // add to text widget; must do in UI thread 280 mText.getDisplay().asyncExec(new Runnable() { 281 public void run() { 282 mText.append(text); 283 } 284 }); 285 } 286 catch (UnsupportedEncodingException uee) { 287 uee.printStackTrace(); // not expected 288 } 289 } 290 291 public void flush() { 292 // nothing to flush. 293 } 294 295 /** 296 * Called by executeRemoteCommand(). 297 */ 298 public boolean isCancelled() { 299 return mCancel; 300 } 301 }; 302 303 /* 304 * Execute a remote command, add the output to the text widget, and 305 * update controls. 306 * 307 * We have to run the command in a thread so that the UI continues 308 * to work. 309 */ 310 private void executeCommand(Shell shell, IDevice device) { 311 Gatherer gath = new Gatherer(shell, device, commandString(), mText); 312 gath.start(); 313 } 314 315 /* 316 * Update the controls after the remote operation completes. This 317 * must be called from the UI thread. 318 */ 319 private void updateForResult(int result) { 320 if (result == Gatherer.RESULT_SUCCESS) { 321 mStatusLabel.setText("Successfully executed '" 322 + shortCommandString() + "'"); 323 mSave.setEnabled(true); 324 } else if (result == Gatherer.RESULT_CANCELLED) { 325 mStatusLabel.setText("Execution cancelled; partial results below"); 326 mSave.setEnabled(true); // save partial 327 } else if (result == Gatherer.RESULT_FAILURE) { 328 mStatusLabel.setText("Failed"); 329 } 330 mStatusLabel.pack(); 331 mCancelDone.setText("Done"); 332 mFinished = true; 333 } 334 335 /* 336 * Allow the user to save the contents of the text dialog. 337 */ 338 private void saveText(Shell shell) { 339 FileDialog dlg = new FileDialog(shell, SWT.SAVE); 340 String fileName; 341 342 dlg.setText("Save output..."); 343 dlg.setFileName(defaultFileName()); 344 dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir")); 345 dlg.setFilterNames(new String[] { 346 "Text Files (*.txt)" 347 }); 348 dlg.setFilterExtensions(new String[] { 349 "*.txt" 350 }); 351 352 fileName = dlg.open(); 353 if (fileName != null) { 354 PrefsDialog.getStore().setValue("lastTextSaveDir", 355 dlg.getFilterPath()); 356 357 Log.d("ddms", "Saving output to " + fileName); 358 359 /* 360 * Convert to 8-bit characters. 361 */ 362 String text = mText.getText(); 363 byte[] ascii; 364 try { 365 ascii = text.getBytes("ISO-8859-1"); 366 } 367 catch (UnsupportedEncodingException uee) { 368 uee.printStackTrace(); 369 ascii = new byte[0]; 370 } 371 372 /* 373 * Output data, converting CRLF to LF. 374 */ 375 try { 376 int length = ascii.length; 377 378 FileOutputStream outFile = new FileOutputStream(fileName); 379 BufferedOutputStream out = new BufferedOutputStream(outFile); 380 for (int i = 0; i < length; i++) { 381 if (i < length-1 && 382 ascii[i] == 0x0d && ascii[i+1] == 0x0a) 383 { 384 continue; 385 } 386 out.write(ascii[i]); 387 } 388 out.close(); // flush buffer, close file 389 } 390 catch (IOException ioe) { 391 Log.w("ddms", "Unable to save " + fileName + ": " + ioe); 392 } 393 } 394 } 395 396 397 /* 398 * Return the shell command we're going to use. 399 */ 400 private String commandString() { 401 return mCommand; 402 403 } 404 405 /* 406 * Return a default filename for the "save" command. 407 */ 408 private String defaultFileName() { 409 return mFileName; 410 } 411 412 /* 413 * Like commandString(), but length-limited. 414 */ 415 private String shortCommandString() { 416 String str = commandString(); 417 if (str.length() > 50) 418 return str.substring(0, 50) + "..."; 419 else 420 return str; 421 } 422 } 423 424