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