1/*
2 * Copyright (C) 2007 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
17package com.android.ddmuilib.explorer;
18
19import com.android.ddmlib.AdbCommandRejectedException;
20import com.android.ddmlib.DdmConstants;
21import com.android.ddmlib.FileListingService;
22import com.android.ddmlib.FileListingService.FileEntry;
23import com.android.ddmlib.IDevice;
24import com.android.ddmlib.IShellOutputReceiver;
25import com.android.ddmlib.ShellCommandUnresponsiveException;
26import com.android.ddmlib.SyncException;
27import com.android.ddmlib.SyncService;
28import com.android.ddmlib.SyncService.ISyncProgressMonitor;
29import com.android.ddmlib.TimeoutException;
30import com.android.ddmuilib.DdmUiPreferences;
31import com.android.ddmuilib.ImageLoader;
32import com.android.ddmuilib.Panel;
33import com.android.ddmuilib.SyncProgressHelper;
34import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
35import com.android.ddmuilib.TableHelper;
36import com.android.ddmuilib.actions.ICommonAction;
37import com.android.ddmuilib.console.DdmConsole;
38
39import org.eclipse.core.runtime.IStatus;
40import org.eclipse.core.runtime.Status;
41import org.eclipse.jface.dialogs.ErrorDialog;
42import org.eclipse.jface.dialogs.IInputValidator;
43import org.eclipse.jface.dialogs.InputDialog;
44import org.eclipse.jface.preference.IPreferenceStore;
45import org.eclipse.jface.viewers.DoubleClickEvent;
46import org.eclipse.jface.viewers.IDoubleClickListener;
47import org.eclipse.jface.viewers.ISelection;
48import org.eclipse.jface.viewers.ISelectionChangedListener;
49import org.eclipse.jface.viewers.IStructuredSelection;
50import org.eclipse.jface.viewers.SelectionChangedEvent;
51import org.eclipse.jface.viewers.TreeViewer;
52import org.eclipse.jface.viewers.ViewerDropAdapter;
53import org.eclipse.swt.SWT;
54import org.eclipse.swt.dnd.DND;
55import org.eclipse.swt.dnd.FileTransfer;
56import org.eclipse.swt.dnd.Transfer;
57import org.eclipse.swt.dnd.TransferData;
58import org.eclipse.swt.graphics.Image;
59import org.eclipse.swt.layout.FillLayout;
60import org.eclipse.swt.widgets.Composite;
61import org.eclipse.swt.widgets.Control;
62import org.eclipse.swt.widgets.DirectoryDialog;
63import org.eclipse.swt.widgets.Display;
64import org.eclipse.swt.widgets.FileDialog;
65import org.eclipse.swt.widgets.Tree;
66import org.eclipse.swt.widgets.TreeItem;
67
68import java.io.BufferedReader;
69import java.io.File;
70import java.io.IOException;
71import java.io.InputStreamReader;
72import java.util.ArrayList;
73import java.util.regex.Matcher;
74import java.util.regex.Pattern;
75
76/**
77 * Device filesystem explorer class.
78 */
79public class DeviceExplorer extends Panel {
80
81    private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S
82    private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S
83
84    private static Pattern mKeyFilePattern = Pattern.compile(
85            "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S
86    private static Pattern mDataFilePattern = Pattern.compile(
87            "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S
88
89    public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S
90    public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S
91    public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S
92    public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S
93    public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S
94    public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S
95
96    private Composite mParent;
97    private TreeViewer mTreeViewer;
98    private Tree mTree;
99    private DeviceContentProvider mContentProvider;
100
101    private ICommonAction mPushAction;
102    private ICommonAction mPullAction;
103    private ICommonAction mDeleteAction;
104    private ICommonAction mCreateNewFolderAction;
105
106    private Image mFileImage;
107    private Image mFolderImage;
108    private Image mPackageImage;
109    private Image mOtherImage;
110
111    private IDevice mCurrentDevice;
112
113    private String mDefaultSave;
114
115    public DeviceExplorer() {
116    }
117
118    /**
119     * Sets custom images for the device explorer. If none are set then defaults are used.
120     * This can be useful to set platform-specific explorer icons.
121     *
122     * This should be called before {@link #createControl(Composite)}.
123     *
124     * @param fileImage the icon to represent a file.
125     * @param folderImage the icon to represent a folder.
126     * @param packageImage the icon to represent an apk.
127     * @param otherImage the icon to represent other types of files.
128     */
129    public void setCustomImages(Image fileImage, Image folderImage, Image packageImage,
130            Image otherImage) {
131        mFileImage = fileImage;
132        mFolderImage = folderImage;
133        mPackageImage = packageImage;
134        mOtherImage = otherImage;
135    }
136
137    /**
138     * Sets the actions so that the device explorer can enable/disable them based on the current
139     * selection
140     * @param pushAction
141     * @param pullAction
142     * @param deleteAction
143     * @param createNewFolderAction
144     */
145    public void setActions(ICommonAction pushAction, ICommonAction pullAction,
146            ICommonAction deleteAction, ICommonAction createNewFolderAction) {
147        mPushAction = pushAction;
148        mPullAction = pullAction;
149        mDeleteAction = deleteAction;
150        mCreateNewFolderAction = createNewFolderAction;
151    }
152
153    /**
154     * Creates a control capable of displaying some information.  This is
155     * called once, when the application is initializing, from the UI thread.
156     */
157    @Override
158    protected Control createControl(Composite parent) {
159        mParent = parent;
160        parent.setLayout(new FillLayout());
161
162        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
163        if (mFileImage == null) {
164            mFileImage = loader.loadImage("file.png", mParent.getDisplay());
165        }
166        if (mFolderImage == null) {
167            mFolderImage = loader.loadImage("folder.png", mParent.getDisplay());
168        }
169        if (mPackageImage == null) {
170            mPackageImage = loader.loadImage("android.png", mParent.getDisplay());
171        }
172        if (mOtherImage == null) {
173            // TODO: find a default image for other.
174        }
175
176        mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);
177        mTree.setHeaderVisible(true);
178
179        IPreferenceStore store = DdmUiPreferences.getStore();
180
181        // create columns
182        TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
183                "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$
184        TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT,
185                "000000", COLUMN_SIZE, store); //$NON-NLS-1$
186        TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT,
187                "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$
188        TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT,
189                "20:54", COLUMN_TIME, store); //$NON-NLS-1$
190        TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT,
191                "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$
192        TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT,
193                "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$
194
195        // create the jface wrapper
196        mTreeViewer = new TreeViewer(mTree);
197
198        // setup data provider
199        mContentProvider = new DeviceContentProvider();
200        mTreeViewer.setContentProvider(mContentProvider);
201        mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage,
202                mFolderImage, mPackageImage, mOtherImage));
203
204        // setup a listener for selection
205        mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
206            @Override
207            public void selectionChanged(SelectionChangedEvent event) {
208                ISelection sel = event.getSelection();
209                if (sel.isEmpty()) {
210                    mPullAction.setEnabled(false);
211                    mPushAction.setEnabled(false);
212                    mDeleteAction.setEnabled(false);
213                    mCreateNewFolderAction.setEnabled(false);
214                    return;
215                }
216                if (sel instanceof IStructuredSelection) {
217                    IStructuredSelection selection = (IStructuredSelection) sel;
218                    Object element = selection.getFirstElement();
219                    if (element == null)
220                        return;
221                    if (element instanceof FileEntry) {
222                        mPullAction.setEnabled(true);
223                        mPushAction.setEnabled(selection.size() == 1);
224                        if (selection.size() == 1) {
225                            FileEntry entry = (FileEntry) element;
226                            setDeleteEnabledState(entry);
227                            mCreateNewFolderAction.setEnabled(entry.isDirectory());
228                        } else {
229                            mDeleteAction.setEnabled(false);
230                        }
231                    }
232                }
233            }
234        });
235
236        // add support for double click
237        mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
238            @Override
239            public void doubleClick(DoubleClickEvent event) {
240                ISelection sel = event.getSelection();
241
242                if (sel instanceof IStructuredSelection) {
243                    IStructuredSelection selection = (IStructuredSelection) sel;
244
245                    if (selection.size() == 1) {
246                        FileEntry entry = (FileEntry)selection.getFirstElement();
247                        String name = entry.getName();
248
249                        FileEntry parentEntry = entry.getParent();
250
251                        // can't really do anything with no parent
252                        if (parentEntry == null) {
253                            return;
254                        }
255
256                        // check this is a file like we want.
257                        Matcher m = mKeyFilePattern.matcher(name);
258                        if (m.matches()) {
259                            // get the name w/o the extension
260                            String baseName = m.group(1);
261
262                            // add the data extension
263                            String dataName = baseName + TRACE_DATA_EXT;
264
265                            FileEntry dataEntry = parentEntry.findChild(dataName);
266
267                            handleTraceDoubleClick(baseName, entry, dataEntry);
268
269                        } else {
270                            m = mDataFilePattern.matcher(name);
271                            if (m.matches()) {
272                                // get the name w/o the extension
273                                String baseName = m.group(1);
274
275                                // add the key extension
276                                String keyName = baseName + TRACE_KEY_EXT;
277
278                                FileEntry keyEntry = parentEntry.findChild(keyName);
279
280                                handleTraceDoubleClick(baseName, keyEntry, entry);
281                            }
282                        }
283                    }
284                }
285            }
286        });
287
288        // setup drop listener
289        mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE,
290                new Transfer[] { FileTransfer.getInstance() },
291                new ViewerDropAdapter(mTreeViewer) {
292            @Override
293            public boolean performDrop(Object data) {
294                // get the item on which we dropped the item(s)
295                FileEntry target = (FileEntry)getCurrentTarget();
296
297                // in case we drop at the same level as root
298                if (target == null) {
299                    return false;
300                }
301
302                // if the target is not a directory, we get the parent directory
303                if (target.isDirectory() == false) {
304                    target = target.getParent();
305                }
306
307                if (target == null) {
308                    return false;
309                }
310
311                // get the list of files to drop
312                String[] files = (String[])data;
313
314                // do the drop
315                pushFiles(files, target);
316
317                // we need to finish with a refresh
318                refresh(target);
319
320                return true;
321            }
322
323            @Override
324            public boolean validateDrop(Object target, int operation, TransferData transferType) {
325                if (target == null) {
326                    return false;
327                }
328
329                // convert to the real item
330                FileEntry targetEntry = (FileEntry)target;
331
332                // if the target is not a directory, we get the parent directory
333                if (targetEntry.isDirectory() == false) {
334                    target = targetEntry.getParent();
335                }
336
337                if (target == null) {
338                    return false;
339                }
340
341                return true;
342            }
343        });
344
345        // create and start the refresh thread
346        new Thread("Device Ls refresher") {
347            @Override
348            public void run() {
349                while (true) {
350                    try {
351                        sleep(FileListingService.REFRESH_RATE);
352                    } catch (InterruptedException e) {
353                        return;
354                    }
355
356                    if (mTree != null && mTree.isDisposed() == false) {
357                        Display display = mTree.getDisplay();
358                        if (display.isDisposed() == false) {
359                            display.asyncExec(new Runnable() {
360                                @Override
361                                public void run() {
362                                    if (mTree.isDisposed() == false) {
363                                        mTreeViewer.refresh(true);
364                                    }
365                                }
366                            });
367                        } else {
368                            return;
369                        }
370                    } else {
371                        return;
372                    }
373                }
374
375            }
376        }.start();
377
378        return mTree;
379    }
380
381    @Override
382    protected void postCreation() {
383
384    }
385
386    /**
387     * Sets the focus to the proper control inside the panel.
388     */
389    @Override
390    public void setFocus() {
391        mTree.setFocus();
392    }
393
394    /**
395     * Processes a double click on a trace file
396     * @param baseName the base name of the 2 files.
397     * @param keyEntry The FileEntry for the .key file.
398     * @param dataEntry The FileEntry for the .data file.
399     */
400    private void handleTraceDoubleClick(String baseName, FileEntry keyEntry,
401            FileEntry dataEntry) {
402        // first we need to download the files.
403        File keyFile;
404        File dataFile;
405        String path;
406        try {
407            // create a temp file for keyFile
408            File f = File.createTempFile(baseName, DdmConstants.DOT_TRACE);
409            f.delete();
410            f.mkdir();
411
412            path = f.getAbsolutePath();
413
414            keyFile = new File(path + File.separator + keyEntry.getName());
415            dataFile = new File(path + File.separator + dataEntry.getName());
416        } catch (IOException e) {
417            return;
418        }
419
420        // download the files
421        try {
422            SyncService sync = mCurrentDevice.getSyncService();
423            if (sync != null) {
424                ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
425                sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
426                sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
427
428                // now that we have the file, we need to launch traceview
429                String[] command = new String[2];
430                command[0] = DdmUiPreferences.getTraceview();
431                command[1] = path + File.separator + baseName;
432
433                try {
434                    final Process p = Runtime.getRuntime().exec(command);
435
436                    // create a thread for the output
437                    new Thread("Traceview output") {
438                        @Override
439                        public void run() {
440                            // create a buffer to read the stderr output
441                            InputStreamReader is = new InputStreamReader(p.getErrorStream());
442                            BufferedReader resultReader = new BufferedReader(is);
443
444                            // read the lines as they come. if null is returned, it's
445                            // because the process finished
446                            try {
447                                while (true) {
448                                    String line = resultReader.readLine();
449                                    if (line != null) {
450                                        DdmConsole.printErrorToConsole("Traceview: " + line);
451                                    } else {
452                                        break;
453                                    }
454                                }
455                                // get the return code from the process
456                                p.waitFor();
457                            } catch (IOException e) {
458                            } catch (InterruptedException e) {
459
460                            }
461                        }
462                    }.start();
463
464                } catch (IOException e) {
465                }
466            }
467        } catch (IOException e) {
468            DdmConsole.printErrorToConsole(String.format(
469                    "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
470            return;
471        } catch (SyncException e) {
472            if (e.wasCanceled() == false) {
473                DdmConsole.printErrorToConsole(String.format(
474                        "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
475                return;
476            }
477        } catch (TimeoutException e) {
478            DdmConsole.printErrorToConsole(String.format(
479                    "Failed to pull %1$s: timeout", keyEntry.getName()));
480        } catch (AdbCommandRejectedException e) {
481            DdmConsole.printErrorToConsole(String.format(
482                    "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
483        }
484    }
485
486    /**
487     * Pull the current selection on the local drive. This method displays
488     * a dialog box to let the user select where to store the file(s) and
489     * folder(s).
490     */
491    public void pullSelection() {
492        // get the selection
493        TreeItem[] items = mTree.getSelection();
494
495        // name of the single file pull, or null if we're pulling a directory
496        // or more than one object.
497        String filePullName = null;
498        FileEntry singleEntry = null;
499
500        // are we pulling a single file?
501        if (items.length == 1) {
502            singleEntry = (FileEntry)items[0].getData();
503            if (singleEntry.getType() == FileListingService.TYPE_FILE) {
504                filePullName = singleEntry.getName();
505            }
506        }
507
508        // where do we save by default?
509        String defaultPath = mDefaultSave;
510        if (defaultPath == null) {
511            defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
512        }
513
514        if (filePullName != null) {
515            FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
516
517            fileDialog.setText("Get Device File");
518            fileDialog.setFileName(filePullName);
519            fileDialog.setFilterPath(defaultPath);
520
521            String fileName = fileDialog.open();
522            if (fileName != null) {
523                mDefaultSave = fileDialog.getFilterPath();
524
525                pullFile(singleEntry, fileName);
526            }
527        } else {
528            DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE);
529
530            directoryDialog.setText("Get Device Files/Folders");
531            directoryDialog.setFilterPath(defaultPath);
532
533            String directoryName = directoryDialog.open();
534            if (directoryName != null) {
535                pullSelection(items, directoryName);
536            }
537        }
538    }
539
540    /**
541     * Push new file(s) and folder(s) into the current selection. Current
542     * selection must be single item. If the current selection is not a
543     * directory, the parent directory is used.
544     * This method displays a dialog to let the user choose file to push to
545     * the device.
546     */
547    public void pushIntoSelection() {
548        // get the name of the object we're going to pull
549        TreeItem[] items = mTree.getSelection();
550
551        if (items.length == 0) {
552            return;
553        }
554
555        FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN);
556        String fileName;
557
558        dlg.setText("Put File on Device");
559
560        // There should be only one.
561        FileEntry entry = (FileEntry)items[0].getData();
562        dlg.setFileName(entry.getName());
563
564        String defaultPath = mDefaultSave;
565        if (defaultPath == null) {
566            defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
567        }
568        dlg.setFilterPath(defaultPath);
569
570        fileName = dlg.open();
571        if (fileName != null) {
572            mDefaultSave = dlg.getFilterPath();
573
574            // we need to figure out the remote path based on the current selection type.
575            String remotePath;
576            FileEntry toRefresh = entry;
577            if (entry.isDirectory()) {
578                remotePath = entry.getFullPath();
579            } else {
580                toRefresh = entry.getParent();
581                remotePath = toRefresh.getFullPath();
582            }
583
584            pushFile(fileName, remotePath);
585            mTreeViewer.refresh(toRefresh);
586        }
587    }
588
589    public void deleteSelection() {
590        // get the name of the object we're going to pull
591        TreeItem[] items = mTree.getSelection();
592
593        if (items.length != 1) {
594            return;
595        }
596
597        FileEntry entry = (FileEntry)items[0].getData();
598        final FileEntry parentEntry = entry.getParent();
599
600        // create the delete command
601        String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$
602
603        try {
604            mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
605                @Override
606                public void addOutput(byte[] data, int offset, int length) {
607                    // pass
608                    // TODO get output to display errors if any.
609                }
610
611                @Override
612                public void flush() {
613                    mTreeViewer.refresh(parentEntry);
614                }
615
616                @Override
617                public boolean isCancelled() {
618                    return false;
619                }
620            });
621        } catch (IOException e) {
622            // adb failed somehow, we do nothing. We should be displaying the error from the output
623            // of the shell command.
624        } catch (TimeoutException e) {
625            // adb failed somehow, we do nothing. We should be displaying the error from the output
626            // of the shell command.
627        } catch (AdbCommandRejectedException e) {
628            // adb failed somehow, we do nothing. We should be displaying the error from the output
629            // of the shell command.
630        } catch (ShellCommandUnresponsiveException e) {
631            // adb failed somehow, we do nothing. We should be displaying the error from the output
632            // of the shell command.
633        }
634
635    }
636
637    public void createNewFolderInSelection() {
638        TreeItem[] items = mTree.getSelection();
639
640        if (items.length != 1) {
641            return;
642        }
643
644        final FileEntry entry = (FileEntry) items[0].getData();
645
646        if (entry.isDirectory()) {
647            InputDialog inputDialog = new InputDialog(mTree.getShell(), "New Folder",
648                    "Please enter the new folder name", "New Folder", new IInputValidator() {
649                        @Override
650                        public String isValid(String newText) {
651                            if ((newText != null) && (newText.length() > 0)
652                                    && (newText.trim().length() > 0)
653                                    && (newText.indexOf('/') == -1)
654                                    && (newText.indexOf('\\') == -1)) {
655                                return null;
656                            } else {
657                                return "Invalid name";
658                            }
659                        }
660                    });
661            inputDialog.open();
662            String value = inputDialog.getValue();
663
664            if (value != null) {
665                // create the mkdir command
666                String command = "mkdir " + entry.getFullEscapedPath() //$NON-NLS-1$
667                        + FileListingService.FILE_SEPARATOR + FileEntry.escape(value);
668
669                try {
670                    mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
671
672                        @Override
673                        public boolean isCancelled() {
674                            return false;
675                        }
676
677                        @Override
678                        public void flush() {
679                            mTreeViewer.refresh(entry);
680                        }
681
682                        @Override
683                        public void addOutput(byte[] data, int offset, int length) {
684                            String errorMessage;
685                            if (data != null) {
686                                errorMessage = new String(data);
687                            } else {
688                                errorMessage = "";
689                            }
690                            Status status = new Status(IStatus.ERROR,
691                                    "DeviceExplorer", 0, errorMessage, null); //$NON-NLS-1$
692                            ErrorDialog.openError(mTree.getShell(), "New Folder Error",
693                                    "New Folder Error", status);
694                        }
695                    });
696                } catch (TimeoutException e) {
697                    // adb failed somehow, we do nothing. We should be
698                    // displaying the error from the output of the shell
699                    // command.
700                } catch (AdbCommandRejectedException e) {
701                    // adb failed somehow, we do nothing. We should be
702                    // displaying the error from the output of the shell
703                    // command.
704                } catch (ShellCommandUnresponsiveException e) {
705                    // adb failed somehow, we do nothing. We should be
706                    // displaying the error from the output of the shell
707                    // command.
708                } catch (IOException e) {
709                    // adb failed somehow, we do nothing. We should be
710                    // displaying the error from the output of the shell
711                    // command.
712                }
713            }
714        }
715    }
716
717    /**
718     * Force a full refresh of the explorer.
719     */
720    public void refresh() {
721        mTreeViewer.refresh(true);
722    }
723
724    /**
725     * Sets the new device to explorer
726     */
727    public void switchDevice(final IDevice device) {
728        if (device != mCurrentDevice) {
729            mCurrentDevice = device;
730            // now we change the input. but we need to do that in the
731            // ui thread.
732            if (mTree.isDisposed() == false) {
733                Display d = mTree.getDisplay();
734                d.asyncExec(new Runnable() {
735                    @Override
736                    public void run() {
737                        if (mTree.isDisposed() == false) {
738                            // new service
739                            if (mCurrentDevice != null) {
740                                FileListingService fls = mCurrentDevice.getFileListingService();
741                                mContentProvider.setListingService(fls);
742                                mTreeViewer.setInput(fls.getRoot());
743                            }
744                        }
745                    }
746                });
747            }
748        }
749    }
750
751    /**
752     * Refresh an entry from a non ui thread.
753     * @param entry the entry to refresh.
754     */
755    private void refresh(final FileEntry entry) {
756        Display d = mTreeViewer.getTree().getDisplay();
757        d.asyncExec(new Runnable() {
758            @Override
759            public void run() {
760                mTreeViewer.refresh(entry);
761            }
762        });
763    }
764
765    /**
766     * Pulls the selection from a device.
767     * @param items the tree selection the remote file on the device
768     * @param localDirector the local directory in which to save the files.
769     */
770    private void pullSelection(TreeItem[] items, final String localDirectory) {
771        try {
772            final SyncService sync = mCurrentDevice.getSyncService();
773            if (sync != null) {
774                // make a list of the FileEntry.
775                ArrayList<FileEntry> entries = new ArrayList<FileEntry>();
776                for (TreeItem item : items) {
777                    Object data = item.getData();
778                    if (data instanceof FileEntry) {
779                        entries.add((FileEntry)data);
780                    }
781                }
782                final FileEntry[] entryArray = entries.toArray(
783                        new FileEntry[entries.size()]);
784
785                SyncProgressHelper.run(new SyncRunnable() {
786                    @Override
787                    public void run(ISyncProgressMonitor monitor)
788                            throws SyncException, IOException, TimeoutException {
789                        sync.pull(entryArray, localDirectory, monitor);
790                    }
791
792                    @Override
793                    public void close() {
794                        sync.close();
795                    }
796                }, "Pulling file(s) from the device", mParent.getShell());
797            }
798        } catch (SyncException e) {
799            if (e.wasCanceled() == false) {
800                DdmConsole.printErrorToConsole(String.format(
801                        "Failed to pull selection: %1$s", e.getMessage()));
802            }
803        } catch (Exception e) {
804            DdmConsole.printErrorToConsole( "Failed to pull selection");
805            DdmConsole.printErrorToConsole(e.getMessage());
806        }
807    }
808
809    /**
810     * Pulls a file from a device.
811     * @param remote the remote file on the device
812     * @param local the destination filepath
813     */
814    private void pullFile(final FileEntry remote, final String local) {
815        try {
816            final SyncService sync = mCurrentDevice.getSyncService();
817            if (sync != null) {
818                SyncProgressHelper.run(new SyncRunnable() {
819                        @Override
820                        public void run(ISyncProgressMonitor monitor)
821                                throws SyncException, IOException, TimeoutException {
822                            sync.pullFile(remote, local, monitor);
823                        }
824
825                        @Override
826                        public void close() {
827                            sync.close();
828                        }
829                    }, String.format("Pulling %1$s from the device", remote.getName()),
830                    mParent.getShell());
831            }
832        } catch (SyncException e) {
833            if (e.wasCanceled() == false) {
834                DdmConsole.printErrorToConsole(String.format(
835                        "Failed to pull selection: %1$s", e.getMessage()));
836            }
837        } catch (Exception e) {
838            DdmConsole.printErrorToConsole( "Failed to pull selection");
839            DdmConsole.printErrorToConsole(e.getMessage());
840        }
841    }
842
843    /**
844     * Pushes several files and directory into a remote directory.
845     * @param localFiles
846     * @param remoteDirectory
847     */
848    private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) {
849        try {
850            final SyncService sync = mCurrentDevice.getSyncService();
851            if (sync != null) {
852                SyncProgressHelper.run(new SyncRunnable() {
853                        @Override
854                        public void run(ISyncProgressMonitor monitor)
855                                throws SyncException, IOException, TimeoutException {
856                            sync.push(localFiles, remoteDirectory, monitor);
857                        }
858
859                        @Override
860                        public void close() {
861                            sync.close();
862                        }
863                    }, "Pushing file(s) to the device", mParent.getShell());
864            }
865        } catch (SyncException e) {
866            if (e.wasCanceled() == false) {
867                DdmConsole.printErrorToConsole(String.format(
868                        "Failed to push selection: %1$s", e.getMessage()));
869            }
870        } catch (Exception e) {
871            DdmConsole.printErrorToConsole("Failed to push the items");
872            DdmConsole.printErrorToConsole(e.getMessage());
873        }
874    }
875
876    /**
877     * Pushes a file on a device.
878     * @param local the local filepath of the file to push
879     * @param remoteDirectory the remote destination directory on the device
880     */
881    private void pushFile(final String local, final String remoteDirectory) {
882        try {
883            final SyncService sync = mCurrentDevice.getSyncService();
884            if (sync != null) {
885                // get the file name
886                String[] segs = local.split(Pattern.quote(File.separator));
887                String name = segs[segs.length-1];
888                final String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
889                        + name;
890
891                SyncProgressHelper.run(new SyncRunnable() {
892                        @Override
893                        public void run(ISyncProgressMonitor monitor)
894                                throws SyncException, IOException, TimeoutException {
895                            sync.pushFile(local, remoteFile, monitor);
896                        }
897
898                        @Override
899                        public void close() {
900                            sync.close();
901                        }
902                    }, String.format("Pushing %1$s to the device.", name), mParent.getShell());
903            }
904        } catch (SyncException e) {
905            if (e.wasCanceled() == false) {
906                DdmConsole.printErrorToConsole(String.format(
907                        "Failed to push selection: %1$s", e.getMessage()));
908            }
909        } catch (Exception e) {
910            DdmConsole.printErrorToConsole("Failed to push the item(s).");
911            DdmConsole.printErrorToConsole(e.getMessage());
912        }
913    }
914
915    /**
916     * Sets the enabled state based on a FileEntry properties
917     * @param element The selected FileEntry
918     */
919    protected void setDeleteEnabledState(FileEntry element) {
920        mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE);
921    }
922}
923