2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.ddmuilib.explorer;
19 import com.android.ddmlib.IDevice;
20 import com.android.ddmlib.FileListingService;
21 import com.android.ddmlib.IShellOutputReceiver;
22 import com.android.ddmlib.SyncService;
23 import com.android.ddmlib.FileListingService.FileEntry;
24 import com.android.ddmlib.SyncService.ISyncProgressMonitor;
25 import com.android.ddmlib.SyncService.SyncResult;
26 import com.android.ddmuilib.DdmUiPreferences;
27 import com.android.ddmuilib.Panel;
28 import com.android.ddmuilib.SyncProgressMonitor;
29 import com.android.ddmuilib.TableHelper;
30 import com.android.ddmuilib.actions.ICommonAction;
31 import com.android.ddmuilib.console.DdmConsole;
33 import org.eclipse.core.runtime.IProgressMonitor;
34 import org.eclipse.jface.dialogs.ProgressMonitorDialog;
35 import org.eclipse.jface.operation.IRunnableWithProgress;
36 import org.eclipse.jface.preference.IPreferenceStore;
37 import org.eclipse.jface.viewers.DoubleClickEvent;
38 import org.eclipse.jface.viewers.IDoubleClickListener;
39 import org.eclipse.jface.viewers.ISelection;
40 import org.eclipse.jface.viewers.ISelectionChangedListener;
41 import org.eclipse.jface.viewers.IStructuredSelection;
42 import org.eclipse.jface.viewers.SelectionChangedEvent;
43 import org.eclipse.jface.viewers.TreeViewer;
44 import org.eclipse.jface.viewers.ViewerDropAdapter;
45 import org.eclipse.swt.SWT;
46 import org.eclipse.swt.dnd.DND;
47 import org.eclipse.swt.dnd.FileTransfer;
48 import org.eclipse.swt.dnd.Transfer;
49 import org.eclipse.swt.dnd.TransferData;
50 import org.eclipse.swt.graphics.Image;
51 import org.eclipse.swt.layout.FillLayout;
52 import org.eclipse.swt.widgets.Composite;
53 import org.eclipse.swt.widgets.Control;
54 import org.eclipse.swt.widgets.DirectoryDialog;
55 import org.eclipse.swt.widgets.Display;
56 import org.eclipse.swt.widgets.FileDialog;
57 import org.eclipse.swt.widgets.Tree;
58 import org.eclipse.swt.widgets.TreeItem;
60 import java.io.BufferedReader;
62 import java.io.IOException;
63 import java.io.InputStreamReader;
64 import java.lang.reflect.InvocationTargetException;
65 import java.util.ArrayList;
66 import java.util.regex.Matcher;
67 import java.util.regex.Pattern;
70 * Device filesystem explorer class.
72 public class DeviceExplorer extends Panel {
74 private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S
75 private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S
77 private static Pattern mKeyFilePattern = Pattern.compile(
78 "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S
79 private static Pattern mDataFilePattern = Pattern.compile(
80 "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S
82 public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S
83 public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S
84 public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S
85 public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S
86 public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S
87 public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S
89 private Composite mParent;
90 private TreeViewer mTreeViewer;
92 private DeviceContentProvider mContentProvider;
94 private ICommonAction mPushAction;
95 private ICommonAction mPullAction;
96 private ICommonAction mDeleteAction;
98 private Image mFileImage;
99 private Image mFolderImage;
100 private Image mPackageImage;
101 private Image mOtherImage;
103 private IDevice mCurrentDevice;
105 private String mDefaultSave;
107 public DeviceExplorer() {
112 * Sets the images for the listview
117 public void setImages(Image fileImage, Image folderImage, Image packageImage,
119 mFileImage = fileImage;
120 mFolderImage = folderImage;
121 mPackageImage = packageImage;
122 mOtherImage = otherImage;
126 * Sets the actions so that the device explorer can enable/disable them based on the current
130 * @param deleteAction
132 public void setActions(ICommonAction pushAction, ICommonAction pullAction,
133 ICommonAction deleteAction) {
134 mPushAction = pushAction;
135 mPullAction = pullAction;
136 mDeleteAction = deleteAction;
140 * Creates a control capable of displaying some information. This is
141 * called once, when the application is initializing, from the UI thread.
144 protected Control createControl(Composite parent) {
146 parent.setLayout(new FillLayout());
148 mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);
149 mTree.setHeaderVisible(true);
151 IPreferenceStore store = DdmUiPreferences.getStore();
154 TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
155 "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$
156 TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT,
157 "000000", COLUMN_SIZE, store); //$NON-NLS-1$
158 TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT,
159 "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$
160 TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT,
161 "20:54", COLUMN_TIME, store); //$NON-NLS-1$
162 TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT,
163 "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$
164 TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT,
165 "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$
167 // create the jface wrapper
168 mTreeViewer = new TreeViewer(mTree);
170 // setup data provider
171 mContentProvider = new DeviceContentProvider();
172 mTreeViewer.setContentProvider(mContentProvider);
173 mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage,
174 mFolderImage, mPackageImage, mOtherImage));
176 // setup a listener for selection
177 mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
178 public void selectionChanged(SelectionChangedEvent event) {
179 ISelection sel = event.getSelection();
181 mPullAction.setEnabled(false);
182 mPushAction.setEnabled(false);
183 mDeleteAction.setEnabled(false);
186 if (sel instanceof IStructuredSelection) {
187 IStructuredSelection selection = (IStructuredSelection) sel;
188 Object element = selection.getFirstElement();
191 if (element instanceof FileEntry) {
192 mPullAction.setEnabled(true);
193 mPushAction.setEnabled(selection.size() == 1);
194 if (selection.size() == 1) {
195 setDeleteEnabledState((FileEntry)element);
197 mDeleteAction.setEnabled(false);
204 // add support for double click
205 mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
206 public void doubleClick(DoubleClickEvent event) {
207 ISelection sel = event.getSelection();
209 if (sel instanceof IStructuredSelection) {
210 IStructuredSelection selection = (IStructuredSelection) sel;
212 if (selection.size() == 1) {
213 FileEntry entry = (FileEntry)selection.getFirstElement();
214 String name = entry.getName();
216 FileEntry parentEntry = entry.getParent();
218 // can't really do anything with no parent
219 if (parentEntry == null) {
223 // check this is a file like we want.
224 Matcher m = mKeyFilePattern.matcher(name);
226 // get the name w/o the extension
227 String baseName = m.group(1);
229 // add the data extension
230 String dataName = baseName + TRACE_DATA_EXT;
232 FileEntry dataEntry = parentEntry.findChild(dataName);
234 handleTraceDoubleClick(baseName, entry, dataEntry);
237 m = mDataFilePattern.matcher(name);
239 // get the name w/o the extension
240 String baseName = m.group(1);
242 // add the key extension
243 String keyName = baseName + TRACE_KEY_EXT;
245 FileEntry keyEntry = parentEntry.findChild(keyName);
247 handleTraceDoubleClick(baseName, keyEntry, entry);
255 // setup drop listener
256 mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE,
257 new Transfer[] { FileTransfer.getInstance() },
258 new ViewerDropAdapter(mTreeViewer) {
260 public boolean performDrop(Object data) {
261 // get the item on which we dropped the item(s)
262 FileEntry target = (FileEntry)getCurrentTarget();
264 // in case we drop at the same level as root
265 if (target == null) {
269 // if the target is not a directory, we get the parent directory
270 if (target.isDirectory() == false) {
271 target = target.getParent();
274 if (target == null) {
278 // get the list of files to drop
279 String[] files = (String[])data;
282 pushFiles(files, target);
284 // we need to finish with a refresh
291 public boolean validateDrop(Object target, int operation, TransferData transferType) {
292 if (target == null) {
296 // convert to the real item
297 FileEntry targetEntry = (FileEntry)target;
299 // if the target is not a directory, we get the parent directory
300 if (targetEntry.isDirectory() == false) {
301 target = targetEntry.getParent();
304 if (target == null) {
312 // create and start the refresh thread
313 new Thread("Device Ls refresher") {
318 sleep(FileListingService.REFRESH_RATE);
319 } catch (InterruptedException e) {
323 if (mTree != null && mTree.isDisposed() == false) {
324 Display display = mTree.getDisplay();
325 if (display.isDisposed() == false) {
326 display.asyncExec(new Runnable() {
328 if (mTree.isDisposed() == false) {
329 mTreeViewer.refresh(true);
348 protected void postCreation() {
353 * Sets the focus to the proper control inside the panel.
356 public void setFocus() {
361 * Processes a double click on a trace file
362 * @param baseName the base name of the 2 files.
363 * @param keyEntry The FileEntry for the .key file.
364 * @param dataEntry The FileEntry for the .data file.
366 private void handleTraceDoubleClick(String baseName, FileEntry keyEntry,
367 FileEntry dataEntry) {
368 // first we need to download the files.
373 // create a temp file for keyFile
374 File f = File.createTempFile(baseName, ".trace");
378 path = f.getAbsolutePath();
380 keyFile = new File(path + File.separator + keyEntry.getName());
381 dataFile = new File(path + File.separator + dataEntry.getName());
382 } catch (IOException e) {
386 // download the files
388 SyncService sync = mCurrentDevice.getSyncService();
390 ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
391 SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
392 if (result.getCode() != SyncService.RESULT_OK) {
393 DdmConsole.printErrorToConsole(String.format(
394 "Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage()));
398 result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
399 if (result.getCode() != SyncService.RESULT_OK) {
400 DdmConsole.printErrorToConsole(String.format(
401 "Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage()));
405 // now that we have the file, we need to launch traceview
406 String[] command = new String[2];
407 command[0] = DdmUiPreferences.getTraceview();
408 command[1] = path + File.separator + baseName;
411 final Process p = Runtime.getRuntime().exec(command);
413 // create a thread for the output
414 new Thread("Traceview output") {
417 // create a buffer to read the stderr output
418 InputStreamReader is = new InputStreamReader(p.getErrorStream());
419 BufferedReader resultReader = new BufferedReader(is);
421 // read the lines as they come. if null is returned, it's
422 // because the process finished
425 String line = resultReader.readLine();
427 DdmConsole.printErrorToConsole("Traceview: " + line);
432 // get the return code from the process
434 } catch (IOException e) {
435 } catch (InterruptedException e) {
441 } catch (IOException e) {
444 } catch (IOException e) {
445 DdmConsole.printErrorToConsole(String.format(
446 "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
452 * Pull the current selection on the local drive. This method displays
453 * a dialog box to let the user select where to store the file(s) and
456 public void pullSelection() {
458 TreeItem[] items = mTree.getSelection();
460 // name of the single file pull, or null if we're pulling a directory
461 // or more than one object.
462 String filePullName = null;
463 FileEntry singleEntry = null;
465 // are we pulling a single file?
466 if (items.length == 1) {
467 singleEntry = (FileEntry)items[0].getData();
468 if (singleEntry.getType() == FileListingService.TYPE_FILE) {
469 filePullName = singleEntry.getName();
473 // where do we save by default?
474 String defaultPath = mDefaultSave;
475 if (defaultPath == null) {
476 defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
479 if (filePullName != null) {
480 FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
482 fileDialog.setText("Get Device File");
483 fileDialog.setFileName(filePullName);
484 fileDialog.setFilterPath(defaultPath);
486 String fileName = fileDialog.open();
487 if (fileName != null) {
488 mDefaultSave = fileDialog.getFilterPath();
490 pullFile(singleEntry, fileName);
493 DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE);
495 directoryDialog.setText("Get Device Files/Folders");
496 directoryDialog.setFilterPath(defaultPath);
498 String directoryName = directoryDialog.open();
499 if (directoryName != null) {
500 pullSelection(items, directoryName);
506 * Push new file(s) and folder(s) into the current selection. Current
507 * selection must be single item. If the current selection is not a
508 * directory, the parent directory is used.
509 * This method displays a dialog to let the user choose file to push to
512 public void pushIntoSelection() {
513 // get the name of the object we're going to pull
514 TreeItem[] items = mTree.getSelection();
516 if (items.length == 0) {
520 FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN);
523 dlg.setText("Put File on Device");
525 // There should be only one.
526 FileEntry entry = (FileEntry)items[0].getData();
527 dlg.setFileName(entry.getName());
529 String defaultPath = mDefaultSave;
530 if (defaultPath == null) {
531 defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
533 dlg.setFilterPath(defaultPath);
535 fileName = dlg.open();
536 if (fileName != null) {
537 mDefaultSave = dlg.getFilterPath();
539 // we need to figure out the remote path based on the current selection type.
541 FileEntry toRefresh = entry;
542 if (entry.isDirectory()) {
543 remotePath = entry.getFullPath();
545 toRefresh = entry.getParent();
546 remotePath = toRefresh.getFullPath();
549 pushFile(fileName, remotePath);
550 mTreeViewer.refresh(toRefresh);
554 public void deleteSelection() {
555 // get the name of the object we're going to pull
556 TreeItem[] items = mTree.getSelection();
558 if (items.length != 1) {
562 FileEntry entry = (FileEntry)items[0].getData();
563 final FileEntry parentEntry = entry.getParent();
565 // create the delete command
566 String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$
569 mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
570 public void addOutput(byte[] data, int offset, int length) {
572 // TODO get output to display errors if any.
575 public void flush() {
576 mTreeViewer.refresh(parentEntry);
579 public boolean isCancelled() {
583 } catch (IOException e) {
584 // adb failed somehow, we do nothing. We should be displaying the error from the output
585 // of the shell command.
591 * Force a full refresh of the explorer.
593 public void refresh() {
594 mTreeViewer.refresh(true);
598 * Sets the new device to explorer
600 public void switchDevice(final IDevice device) {
601 if (device != mCurrentDevice) {
602 mCurrentDevice = device;
603 // now we change the input. but we need to do that in the
605 if (mTree.isDisposed() == false) {
606 Display d = mTree.getDisplay();
607 d.asyncExec(new Runnable() {
609 if (mTree.isDisposed() == false) {
611 if (mCurrentDevice != null) {
612 FileListingService fls = mCurrentDevice.getFileListingService();
613 mContentProvider.setListingService(fls);
614 mTreeViewer.setInput(fls.getRoot());
624 * Refresh an entry from a non ui thread.
625 * @param entry the entry to refresh.
627 private void refresh(final FileEntry entry) {
628 Display d = mTreeViewer.getTree().getDisplay();
629 d.asyncExec(new Runnable() {
631 mTreeViewer.refresh(entry);
637 * Pulls the selection from a device.
638 * @param items the tree selection the remote file on the device
639 * @param localDirector the local directory in which to save the files.
641 private void pullSelection(TreeItem[] items, final String localDirectory) {
643 final SyncService sync = mCurrentDevice.getSyncService();
645 // make a list of the FileEntry.
646 ArrayList<FileEntry> entries = new ArrayList<FileEntry>();
647 for (TreeItem item : items) {
648 Object data = item.getData();
649 if (data instanceof FileEntry) {
650 entries.add((FileEntry)data);
653 final FileEntry[] entryArray = entries.toArray(
654 new FileEntry[entries.size()]);
656 // get a progressdialog
657 new ProgressMonitorDialog(mParent.getShell()).run(true, true,
658 new IRunnableWithProgress() {
659 public void run(IProgressMonitor monitor)
660 throws InvocationTargetException,
661 InterruptedException {
662 // create a monitor wrapper around the jface monitor
663 SyncResult result = sync.pull(entryArray, localDirectory,
664 new SyncProgressMonitor(monitor,
665 "Pulling file(s) from the device"));
667 if (result.getCode() != SyncService.RESULT_OK) {
668 DdmConsole.printErrorToConsole(String.format(
669 "Failed to pull selection: %1$s", result.getMessage()));
675 } catch (Exception e) {
676 DdmConsole.printErrorToConsole( "Failed to pull selection");
677 DdmConsole.printErrorToConsole(e.getMessage());
682 * Pulls a file from a device.
683 * @param remote the remote file on the device
684 * @param local the destination filepath
686 private void pullFile(final FileEntry remote, final String local) {
688 final SyncService sync = mCurrentDevice.getSyncService();
690 new ProgressMonitorDialog(mParent.getShell()).run(true, true,
691 new IRunnableWithProgress() {
692 public void run(IProgressMonitor monitor)
693 throws InvocationTargetException,
694 InterruptedException {
695 SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor(
696 monitor, String.format("Pulling %1$s from the device",
698 if (result.getCode() != SyncService.RESULT_OK) {
699 DdmConsole.printErrorToConsole(String.format(
700 "Failed to pull %1$s: %2$s", remote, result.getMessage()));
707 } catch (Exception e) {
708 DdmConsole.printErrorToConsole( "Failed to pull selection");
709 DdmConsole.printErrorToConsole(e.getMessage());
714 * Pushes several files and directory into a remote directory.
716 * @param remoteDirectory
718 private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) {
720 final SyncService sync = mCurrentDevice.getSyncService();
722 new ProgressMonitorDialog(mParent.getShell()).run(true, true,
723 new IRunnableWithProgress() {
724 public void run(IProgressMonitor monitor)
725 throws InvocationTargetException,
726 InterruptedException {
727 SyncResult result = sync.push(localFiles, remoteDirectory,
728 new SyncProgressMonitor(monitor,
729 "Pushing file(s) to the device"));
730 if (result.getCode() != SyncService.RESULT_OK) {
731 DdmConsole.printErrorToConsole(String.format(
732 "Failed to push the items: %1$s", result.getMessage()));
739 } catch (Exception e) {
740 DdmConsole.printErrorToConsole("Failed to push the items");
741 DdmConsole.printErrorToConsole(e.getMessage());
746 * Pushes a file on a device.
747 * @param local the local filepath of the file to push
748 * @param remoteDirectory the remote destination directory on the device
750 private void pushFile(final String local, final String remoteDirectory) {
752 final SyncService sync = mCurrentDevice.getSyncService();
754 new ProgressMonitorDialog(mParent.getShell()).run(true, true,
755 new IRunnableWithProgress() {
756 public void run(IProgressMonitor monitor)
757 throws InvocationTargetException,
758 InterruptedException {
760 String[] segs = local.split(Pattern.quote(File.separator));
761 String name = segs[segs.length-1];
762 String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
765 SyncResult result = sync.pushFile(local, remoteFile,
766 new SyncProgressMonitor(monitor,
767 String.format("Pushing %1$s to the device.", name)));
768 if (result.getCode() != SyncService.RESULT_OK) {
769 DdmConsole.printErrorToConsole(String.format(
770 "Failed to push %1$s on %2$s: %3$s",
771 name, mCurrentDevice.getSerialNumber(), result.getMessage()));
778 } catch (Exception e) {
779 DdmConsole.printErrorToConsole("Failed to push the item(s).");
780 DdmConsole.printErrorToConsole(e.getMessage());
785 * Sets the enabled state based on a FileEntry properties
786 * @param element The selected FileEntry
788 protected void setDeleteEnabledState(FileEntry element) {
789 mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE);