blob: 4652b318603283303c69e2e77f09592550ad7450 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmuilib.explorer;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.FileListingService;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.SyncService;
import com.android.ddmlib.FileListingService.FileEntry;
import com.android.ddmlib.SyncService.ISyncProgressMonitor;
import com.android.ddmlib.SyncService.SyncResult;
import com.android.ddmuilib.DdmUiPreferences;
import com.android.ddmuilib.Panel;
import com.android.ddmuilib.TableHelper;
import com.android.ddmuilib.actions.ICommonAction;
import com.android.ddmuilib.console.DdmConsole;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerDropAdapter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Device filesystem explorer class.
*/
public class DeviceExplorer extends Panel {
private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S
private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S
private static Pattern mKeyFilePattern = Pattern.compile(
"(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S
private static Pattern mDataFilePattern = Pattern.compile(
"(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S
public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S
public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S
public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S
public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S
public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S
public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S
private Composite mParent;
private TreeViewer mTreeViewer;
private Tree mTree;
private DeviceContentProvider mContentProvider;
private ICommonAction mPushAction;
private ICommonAction mPullAction;
private ICommonAction mDeleteAction;
private Image mFileImage;
private Image mFolderImage;
private Image mPackageImage;
private Image mOtherImage;
private IDevice mCurrentDevice;
private String mDefaultSave;
/**
* Implementation of the SyncService.ISyncProgressMonitor. It wraps a jFace IProgressMonitor
* and just forward the calls to the jFace object.
*/
private static class SyncProgressMonitor implements ISyncProgressMonitor {
private IProgressMonitor mMonitor;
private String mName;
SyncProgressMonitor(IProgressMonitor monitor, String name) {
mMonitor = monitor;
mName = name;
}
public void start(int totalWork) {
mMonitor.beginTask(mName, totalWork);
}
public void stop() {
mMonitor.done();
}
public void advance(int work) {
mMonitor.worked(work);
}
public boolean isCanceled() {
return mMonitor.isCanceled();
}
public void startSubTask(String name) {
mMonitor.subTask(name);
}
}
public DeviceExplorer() {
}
/**
* Sets the images for the listview
* @param fileImage
* @param folderImage
* @param otherImage
*/
public void setImages(Image fileImage, Image folderImage, Image packageImage,
Image otherImage) {
mFileImage = fileImage;
mFolderImage = folderImage;
mPackageImage = packageImage;
mOtherImage = otherImage;
}
/**
* Sets the actions so that the device explorer can enable/disable them based on the current
* selection
* @param pushAction
* @param pullAction
* @param deleteAction
*/
public void setActions(ICommonAction pushAction, ICommonAction pullAction,
ICommonAction deleteAction) {
mPushAction = pushAction;
mPullAction = pullAction;
mDeleteAction = deleteAction;
}
/**
* Creates a control capable of displaying some information. This is
* called once, when the application is initializing, from the UI thread.
*/
@Override
protected Control createControl(Composite parent) {
mParent = parent;
parent.setLayout(new FillLayout());
mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);
mTree.setHeaderVisible(true);
IPreferenceStore store = DdmUiPreferences.getStore();
// create columns
TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
"0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$
TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT,
"000000", COLUMN_SIZE, store); //$NON-NLS-1$
TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT,
"2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$
TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT,
"20:54", COLUMN_TIME, store); //$NON-NLS-1$
TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT,
"drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$
TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT,
"drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$
// create the jface wrapper
mTreeViewer = new TreeViewer(mTree);
// setup data provider
mContentProvider = new DeviceContentProvider();
mTreeViewer.setContentProvider(mContentProvider);
mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage,
mFolderImage, mPackageImage, mOtherImage));
// setup a listener for selection
mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
ISelection sel = event.getSelection();
if (sel.isEmpty()) {
mPullAction.setEnabled(false);
mPushAction.setEnabled(false);
mDeleteAction.setEnabled(false);
return;
}
if (sel instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection) sel;
Object element = selection.getFirstElement();
if (element == null)
return;
if (element instanceof FileEntry) {
mPullAction.setEnabled(true);
mPushAction.setEnabled(selection.size() == 1);
if (selection.size() == 1) {
setDeleteEnabledState((FileEntry)element);
} else {
mDeleteAction.setEnabled(false);
}
}
}
}
});
// add support for double click
mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
public void doubleClick(DoubleClickEvent event) {
ISelection sel = event.getSelection();
if (sel instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection) sel;
if (selection.size() == 1) {
FileEntry entry = (FileEntry)selection.getFirstElement();
String name = entry.getName();
FileEntry parentEntry = entry.getParent();
// can't really do anything with no parent
if (parentEntry == null) {
return;
}
// check this is a file like we want.
Matcher m = mKeyFilePattern.matcher(name);
if (m.matches()) {
// get the name w/o the extension
String baseName = m.group(1);
// add the data extension
String dataName = baseName + TRACE_DATA_EXT;
FileEntry dataEntry = parentEntry.findChild(dataName);
handleTraceDoubleClick(baseName, entry, dataEntry);
} else {
m = mDataFilePattern.matcher(name);
if (m.matches()) {
// get the name w/o the extension
String baseName = m.group(1);
// add the key extension
String keyName = baseName + TRACE_KEY_EXT;
FileEntry keyEntry = parentEntry.findChild(keyName);
handleTraceDoubleClick(baseName, keyEntry, entry);
}
}
}
}
}
});
// setup drop listener
mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE,
new Transfer[] { FileTransfer.getInstance() },
new ViewerDropAdapter(mTreeViewer) {
@Override
public boolean performDrop(Object data) {
// get the item on which we dropped the item(s)
FileEntry target = (FileEntry)getCurrentTarget();
// in case we drop at the same level as root
if (target == null) {
return false;
}
// if the target is not a directory, we get the parent directory
if (target.isDirectory() == false) {
target = target.getParent();
}
if (target == null) {
return false;
}
// get the list of files to drop
String[] files = (String[])data;
// do the drop
pushFiles(files, target);
// we need to finish with a refresh
refresh(target);
return true;
}
@Override
public boolean validateDrop(Object target, int operation, TransferData transferType) {
if (target == null) {
return false;
}
// convert to the real item
FileEntry targetEntry = (FileEntry)target;
// if the target is not a directory, we get the parent directory
if (targetEntry.isDirectory() == false) {
target = targetEntry.getParent();
}
if (target == null) {
return false;
}
return true;
}
});
// create and start the refresh thread
new Thread("Device Ls refresher") {
@Override
public void run() {
while (true) {
try {
sleep(FileListingService.REFRESH_RATE);
} catch (InterruptedException e) {
return;
}
if (mTree != null && mTree.isDisposed() == false) {
Display display = mTree.getDisplay();
if (display.isDisposed() == false) {
display.asyncExec(new Runnable() {
public void run() {
if (mTree.isDisposed() == false) {
mTreeViewer.refresh(true);
}
}
});
} else {
return;
}
} else {
return;
}
}
}
}.start();
return mTree;
}
@Override
protected void postCreation() {
}
/**
* Sets the focus to the proper control inside the panel.
*/
@Override
public void setFocus() {
mTree.setFocus();
}
/**
* Processes a double click on a trace file
* @param baseName the base name of the 2 files.
* @param keyEntry The FileEntry for the .key file.
* @param dataEntry The FileEntry for the .data file.
*/
private void handleTraceDoubleClick(String baseName, FileEntry keyEntry,
FileEntry dataEntry) {
// first we need to download the files.
File keyFile;
File dataFile;
String path;
try {
// create a temp file for keyFile
File f = File.createTempFile(baseName, ".trace");
f.delete();
f.mkdir();
path = f.getAbsolutePath();
keyFile = new File(path + File.separator + keyEntry.getName());
dataFile = new File(path + File.separator + dataEntry.getName());
} catch (IOException e) {
return;
}
// download the files
try {
SyncService sync = mCurrentDevice.getSyncService();
if (sync != null) {
ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
if (result.getCode() != SyncService.RESULT_OK) {
DdmConsole.printErrorToConsole(String.format(
"Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage()));
return;
}
result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
if (result.getCode() != SyncService.RESULT_OK) {
DdmConsole.printErrorToConsole(String.format(
"Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage()));
return;
}
// now that we have the file, we need to launch traceview
String[] command = new String[2];
command[0] = DdmUiPreferences.getTraceview();
command[1] = path + File.separator + baseName;
try {
final Process p = Runtime.getRuntime().exec(command);
// create a thread for the output
new Thread("Traceview output") {
@Override
public void run() {
// create a buffer to read the stderr output
InputStreamReader is = new InputStreamReader(p.getErrorStream());
BufferedReader resultReader = new BufferedReader(is);
// read the lines as they come. if null is returned, it's
// because the process finished
try {
while (true) {
String line = resultReader.readLine();
if (line != null) {
DdmConsole.printErrorToConsole("Traceview: " + line);
} else {
break;
}
}
// get the return code from the process
p.waitFor();
} catch (IOException e) {
} catch (InterruptedException e) {
}
}
}.start();
} catch (IOException e) {
}
}
} catch (IOException e) {
DdmConsole.printErrorToConsole(String.format(
"Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
return;
}
}
/**
* Pull the current selection on the local drive. This method displays
* a dialog box to let the user select where to store the file(s) and
* folder(s).
*/
public void pullSelection() {
// get the selection
TreeItem[] items = mTree.getSelection();
// name of the single file pull, or null if we're pulling a directory
// or more than one object.
String filePullName = null;
FileEntry singleEntry = null;
// are we pulling a single file?
if (items.length == 1) {
singleEntry = (FileEntry)items[0].getData();
if (singleEntry.getType() == FileListingService.TYPE_FILE) {
filePullName = singleEntry.getName();
}
}
// where do we save by default?
String defaultPath = mDefaultSave;
if (defaultPath == null) {
defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
}
if (filePullName != null) {
FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
fileDialog.setText("Get Device File");
fileDialog.setFileName(filePullName);
fileDialog.setFilterPath(defaultPath);
String fileName = fileDialog.open();
if (fileName != null) {
mDefaultSave = fileDialog.getFilterPath();
pullFile(singleEntry, fileName);
}
} else {
DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE);
directoryDialog.setText("Get Device Files/Folders");
directoryDialog.setFilterPath(defaultPath);
String directoryName = directoryDialog.open();
if (directoryName != null) {
pullSelection(items, directoryName);
}
}
}
/**
* Push new file(s) and folder(s) into the current selection. Current
* selection must be single item. If the current selection is not a
* directory, the parent directory is used.
* This method displays a dialog to let the user choose file to push to
* the device.
*/
public void pushIntoSelection() {
// get the name of the object we're going to pull
TreeItem[] items = mTree.getSelection();
if (items.length == 0) {
return;
}
FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN);
String fileName;
dlg.setText("Put File on Device");
// There should be only one.
FileEntry entry = (FileEntry)items[0].getData();
dlg.setFileName(entry.getName());
String defaultPath = mDefaultSave;
if (defaultPath == null) {
defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
}
dlg.setFilterPath(defaultPath);
fileName = dlg.open();
if (fileName != null) {
mDefaultSave = dlg.getFilterPath();
// we need to figure out the remote path based on the current selection type.
String remotePath;
FileEntry toRefresh = entry;
if (entry.isDirectory()) {
remotePath = entry.getFullPath();
} else {
toRefresh = entry.getParent();
remotePath = toRefresh.getFullPath();
}
pushFile(fileName, remotePath);
mTreeViewer.refresh(toRefresh);
}
}
public void deleteSelection() {
// get the name of the object we're going to pull
TreeItem[] items = mTree.getSelection();
if (items.length != 1) {
return;
}
FileEntry entry = (FileEntry)items[0].getData();
final FileEntry parentEntry = entry.getParent();
// create the delete command
String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$
try {
mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
public void addOutput(byte[] data, int offset, int length) {
// pass
// TODO get output to display errors if any.
}
public void flush() {
mTreeViewer.refresh(parentEntry);
}
public boolean isCancelled() {
return false;
}
});
} catch (IOException e) {
// adb failed somehow, we do nothing. We should be displaying the error from the output
// of the shell command.
}
}
/**
* Force a full refresh of the explorer.
*/
public void refresh() {
mTreeViewer.refresh(true);
}
/**
* Sets the new device to explorer
*/
public void switchDevice(final IDevice device) {
if (device != mCurrentDevice) {
mCurrentDevice = device;
// now we change the input. but we need to do that in the
// ui thread.
if (mTree.isDisposed() == false) {
Display d = mTree.getDisplay();
d.asyncExec(new Runnable() {
public void run() {
if (mTree.isDisposed() == false) {
// new service
if (mCurrentDevice != null) {
FileListingService fls = mCurrentDevice.getFileListingService();
mContentProvider.setListingService(fls);
mTreeViewer.setInput(fls.getRoot());
}
}
}
});
}
}
}
/**
* Refresh an entry from a non ui thread.
* @param entry the entry to refresh.
*/
private void refresh(final FileEntry entry) {
Display d = mTreeViewer.getTree().getDisplay();
d.asyncExec(new Runnable() {
public void run() {
mTreeViewer.refresh(entry);
}
});
}
/**
* Pulls the selection from a device.
* @param items the tree selection the remote file on the device
* @param localDirector the local directory in which to save the files.
*/
private void pullSelection(TreeItem[] items, final String localDirectory) {
try {
final SyncService sync = mCurrentDevice.getSyncService();
if (sync != null) {
// make a list of the FileEntry.
ArrayList<FileEntry> entries = new ArrayList<FileEntry>();
for (TreeItem item : items) {
Object data = item.getData();
if (data instanceof FileEntry) {
entries.add((FileEntry)data);
}
}
final FileEntry[] entryArray = entries.toArray(
new FileEntry[entries.size()]);
// get a progressdialog
new ProgressMonitorDialog(mParent.getShell()).run(true, true,
new IRunnableWithProgress() {
public void run(IProgressMonitor monitor)
throws InvocationTargetException,
InterruptedException {
// create a monitor wrapper around the jface monitor
SyncResult result = sync.pull(entryArray, localDirectory,
new SyncProgressMonitor(monitor,
"Pulling file(s) from the device"));
if (result.getCode() != SyncService.RESULT_OK) {
DdmConsole.printErrorToConsole(String.format(
"Failed to pull selection: %1$s", result.getMessage()));
}
sync.close();
}
});
}
} catch (Exception e) {
DdmConsole.printErrorToConsole( "Failed to pull selection");
DdmConsole.printErrorToConsole(e.getMessage());
}
}
/**
* Pulls a file from a device.
* @param remote the remote file on the device
* @param local the destination filepath
*/
private void pullFile(final FileEntry remote, final String local) {
try {
final SyncService sync = mCurrentDevice.getSyncService();
if (sync != null) {
new ProgressMonitorDialog(mParent.getShell()).run(true, true,
new IRunnableWithProgress() {
public void run(IProgressMonitor monitor)
throws InvocationTargetException,
InterruptedException {
SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor(
monitor, String.format("Pulling %1$s from the device",
remote.getName())));
if (result.getCode() != SyncService.RESULT_OK) {
DdmConsole.printErrorToConsole(String.format(
"Failed to pull %1$s: %2$s", remote, result.getMessage()));
}
sync.close();
}
});
}
} catch (Exception e) {
DdmConsole.printErrorToConsole( "Failed to pull selection");
DdmConsole.printErrorToConsole(e.getMessage());
}
}
/**
* Pushes several files and directory into a remote directory.
* @param localFiles
* @param remoteDirectory
*/
private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) {
try {
final SyncService sync = mCurrentDevice.getSyncService();
if (sync != null) {
new ProgressMonitorDialog(mParent.getShell()).run(true, true,
new IRunnableWithProgress() {
public void run(IProgressMonitor monitor)
throws InvocationTargetException,
InterruptedException {
SyncResult result = sync.push(localFiles, remoteDirectory,
new SyncProgressMonitor(monitor,
"Pushing file(s) to the device"));
if (result.getCode() != SyncService.RESULT_OK) {
DdmConsole.printErrorToConsole(String.format(
"Failed to push the items: %1$s", result.getMessage()));
}
sync.close();
}
});
}
} catch (Exception e) {
DdmConsole.printErrorToConsole("Failed to push the items");
DdmConsole.printErrorToConsole(e.getMessage());
}
}
/**
* Pushes a file on a device.
* @param local the local filepath of the file to push
* @param remoteDirectory the remote destination directory on the device
*/
private void pushFile(final String local, final String remoteDirectory) {
try {
final SyncService sync = mCurrentDevice.getSyncService();
if (sync != null) {
new ProgressMonitorDialog(mParent.getShell()).run(true, true,
new IRunnableWithProgress() {
public void run(IProgressMonitor monitor)
throws InvocationTargetException,
InterruptedException {
// get the file name
String[] segs = local.split(Pattern.quote(File.separator));
String name = segs[segs.length-1];
String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
+ name;
SyncResult result = sync.pushFile(local, remoteFile,
new SyncProgressMonitor(monitor,
String.format("Pushing %1$s to the device.", name)));
if (result.getCode() != SyncService.RESULT_OK) {
DdmConsole.printErrorToConsole(String.format(
"Failed to push %1$s on %2$s: %3$s",
name, mCurrentDevice.getSerialNumber(), result.getMessage()));
}
sync.close();
}
});
}
} catch (Exception e) {
DdmConsole.printErrorToConsole("Failed to push the item(s).");
DdmConsole.printErrorToConsole(e.getMessage());
}
}
/**
* Sets the enabled state based on a FileEntry properties
* @param element The selected FileEntry
*/
protected void setDeleteEnabledState(FileEntry element) {
mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE);
}
}