blob: 52446ca81317fdde0e7530e9eb7a52695111c18a [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.ddmlib;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provides {@link Device} side file listing service.
* <p/>To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}.
*/
public final class FileListingService {
/** Pattern to find filenames that match "*.apk" */
private static final Pattern sApkPattern =
Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private static final String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$
/** Pattern to parse the output of the 'pm -lf' command.<br>
* The output format looks like:<br>
* /data/app/myapp.apk=com.mypackage.myapp */
private static final Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$
/** Top level data folder. */
public static final String DIRECTORY_DATA = "data"; //$NON-NLS-1$
/** Top level sdcard folder. */
public static final String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$
/** Top level mount folder. */
public static final String DIRECTORY_MNT = "mnt"; //$NON-NLS-1$
/** Top level system folder. */
public static final String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$
/** Top level temp folder. */
public static final String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$
/** Application folder. */
public static final String DIRECTORY_APP = "app"; //$NON-NLS-1$
public static final long REFRESH_RATE = 5000L;
/**
* Refresh test has to be slightly lower for precision issue.
*/
static final long REFRESH_TEST = (long)(REFRESH_RATE * .8);
/** Entry type: File */
public static final int TYPE_FILE = 0;
/** Entry type: Directory */
public static final int TYPE_DIRECTORY = 1;
/** Entry type: Directory Link */
public static final int TYPE_DIRECTORY_LINK = 2;
/** Entry type: Block */
public static final int TYPE_BLOCK = 3;
/** Entry type: Character */
public static final int TYPE_CHARACTER = 4;
/** Entry type: Link */
public static final int TYPE_LINK = 5;
/** Entry type: Socket */
public static final int TYPE_SOCKET = 6;
/** Entry type: FIFO */
public static final int TYPE_FIFO = 7;
/** Entry type: Other */
public static final int TYPE_OTHER = 8;
/** Device side file separator. */
public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$
private static final String FILE_ROOT = "/"; //$NON-NLS-1$
/**
* Regexp pattern to parse the result from ls.
*/
private static final Pattern LS_L_PATTERN = Pattern.compile(
"^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+" +
"([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$
private static final Pattern LS_LD_PATTERN = Pattern.compile(
"d[rwx-]{9}\\s+\\S+\\s+\\S+\\s+[0-9-]{10}\\s+\\d{2}:\\d{2}$"); //$NON-NLS-1$
private Device mDevice;
private FileEntry mRoot;
private ArrayList<Thread> mThreadList = new ArrayList<Thread>();
/**
* Represents an entry in a directory. This can be a file or a directory.
*/
public static final class FileEntry {
/** Pattern to escape filenames for shell command consumption.
* This pattern identifies any special characters that need to be escaped with a
* backslash. */
private static final Pattern sEscapePattern = Pattern.compile(
"([\\\\()*+?\"'&#/\\s])"); //$NON-NLS-1$
/**
* Comparator object for FileEntry
*/
private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() {
@Override
public int compare(FileEntry o1, FileEntry o2) {
if (o1 instanceof FileEntry && o2 instanceof FileEntry) {
FileEntry fe1 = o1;
FileEntry fe2 = o2;
return fe1.name.compareTo(fe2.name);
}
return 0;
}
};
FileEntry parent;
String name;
String info;
String permissions;
String size;
String date;
String time;
String owner;
String group;
int type;
boolean isAppPackage;
boolean isRoot;
/**
* Indicates whether the entry content has been fetched yet, or not.
*/
long fetchTime = 0;
final ArrayList<FileEntry> mChildren = new ArrayList<FileEntry>();
/**
* Creates a new file entry.
* @param parent parent entry or null if entry is root
* @param name name of the entry.
* @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE},
* {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}.
*/
private FileEntry(FileEntry parent, String name, int type, boolean isRoot) {
this.parent = parent;
this.name = name;
this.type = type;
this.isRoot = isRoot;
checkAppPackageStatus();
}
/**
* Returns the name of the entry
*/
public String getName() {
return name;
}
/**
* Returns the size string of the entry, as returned by <code>ls</code>.
*/
public String getSize() {
return size;
}
/**
* Returns the size of the entry.
*/
public int getSizeValue() {
return Integer.parseInt(size);
}
/**
* Returns the date string of the entry, as returned by <code>ls</code>.
*/
public String getDate() {
return date;
}
/**
* Returns the time string of the entry, as returned by <code>ls</code>.
*/
public String getTime() {
return time;
}
/**
* Returns the permission string of the entry, as returned by <code>ls</code>.
*/
public String getPermissions() {
return permissions;
}
/**
* Returns the owner string of the entry, as returned by <code>ls</code>.
*/
public String getOwner() {
return owner;
}
/**
* Returns the group owner of the entry, as returned by <code>ls</code>.
*/
public String getGroup() {
return group;
}
/**
* Returns the extra info for the entry.
* <p/>For a link, it will be a description of the link.
* <p/>For an application apk file it will be the application package as returned
* by the Package Manager.
*/
public String getInfo() {
return info;
}
/**
* Return the full path of the entry.
* @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator.
*/
public String getFullPath() {
if (isRoot) {
return FILE_ROOT;
}
StringBuilder pathBuilder = new StringBuilder();
fillPathBuilder(pathBuilder, false);
return pathBuilder.toString();
}
/**
* Return the fully escaped path of the entry. This path is safe to use in a
* shell command line.
* @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator
*/
public String getFullEscapedPath() {
StringBuilder pathBuilder = new StringBuilder();
fillPathBuilder(pathBuilder, true);
return pathBuilder.toString();
}
/**
* Returns the path as a list of segments.
*/
public String[] getPathSegments() {
ArrayList<String> list = new ArrayList<String>();
fillPathSegments(list);
return list.toArray(new String[list.size()]);
}
/**
* Returns the Entry type as an int, which will match one of the TYPE_(...) constants
*/
public int getType() {
return type;
}
/**
* Sets a new type.
*/
public void setType(int type) {
this.type = type;
}
/**
* Returns if the entry is a folder or a link to a folder.
*/
public boolean isDirectory() {
return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK;
}
/**
* Returns the parent entry.
*/
public FileEntry getParent() {
return parent;
}
/**
* Returns the cached children of the entry. This returns the cache created from calling
* <code>FileListingService.getChildren()</code>.
*/
public FileEntry[] getCachedChildren() {
return mChildren.toArray(new FileEntry[mChildren.size()]);
}
/**
* Returns the child {@link FileEntry} matching the name.
* This uses the cached children list.
* @param name the name of the child to return.
* @return the FileEntry matching the name or null.
*/
public FileEntry findChild(String name) {
for (FileEntry entry : mChildren) {
if (entry.name.equals(name)) {
return entry;
}
}
return null;
}
/**
* Returns whether the entry is the root.
*/
public boolean isRoot() {
return isRoot;
}
void addChild(FileEntry child) {
mChildren.add(child);
}
void setChildren(ArrayList<FileEntry> newChildren) {
mChildren.clear();
mChildren.addAll(newChildren);
}
boolean needFetch() {
if (fetchTime == 0) {
return true;
}
long current = System.currentTimeMillis();
return current - fetchTime > REFRESH_TEST;
}
/**
* Returns if the entry is a valid application package.
*/
public boolean isApplicationPackage() {
return isAppPackage;
}
/**
* Returns if the file name is an application package name.
*/
public boolean isAppFileName() {
Matcher m = sApkPattern.matcher(name);
return m.matches();
}
/**
* Recursively fills the pathBuilder with the full path
* @param pathBuilder a StringBuilder used to create the path.
* @param escapePath Whether the path need to be escaped for consumption by
* a shell command line.
*/
protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) {
if (isRoot) {
return;
}
if (parent != null) {
parent.fillPathBuilder(pathBuilder, escapePath);
}
pathBuilder.append(FILE_SEPARATOR);
pathBuilder.append(escapePath ? escape(name) : name);
}
/**
* Recursively fills the segment list with the full path.
* @param list The list of segments to fill.
*/
protected void fillPathSegments(ArrayList<String> list) {
if (isRoot) {
return;
}
if (parent != null) {
parent.fillPathSegments(list);
}
list.add(name);
}
/**
* Sets the internal app package status flag. This checks whether the entry is in an app
* directory like /data/app or /system/app
*/
private void checkAppPackageStatus() {
isAppPackage = false;
String[] segments = getPathSegments();
if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) {
isAppPackage = DIRECTORY_APP.equals(segments[1]) &&
(DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0]));
}
}
/**
* Returns an escaped version of the entry name.
* @param entryName
*/
public static String escape(String entryName) {
return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$
}
}
private static class LsReceiver extends MultiLineReceiver {
private ArrayList<FileEntry> mEntryList;
private ArrayList<String> mLinkList;
private FileEntry[] mCurrentChildren;
private FileEntry mParentEntry;
/**
* Create an ls receiver/parser.
* @param currentChildren The list of current children. To prevent
* collapse during update, reusing the same FileEntry objects for
* files that were already there is paramount.
* @param entryList the list of new children to be filled by the
* receiver.
* @param linkList the list of link path to compute post ls, to figure
* out if the link pointed to a file or to a directory.
*/
public LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList,
ArrayList<String> linkList) {
mParentEntry = parentEntry;
mCurrentChildren = parentEntry.getCachedChildren();
mEntryList = entryList;
mLinkList = linkList;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
// no need to handle empty lines.
if (line.isEmpty()) {
continue;
}
// run the line through the regexp
Matcher m = LS_L_PATTERN.matcher(line);
if (!m.matches()) {
continue;
}
// get the name
String name = m.group(7);
// get the rest of the groups
String permissions = m.group(1);
String owner = m.group(2);
String group = m.group(3);
String size = m.group(4);
String date = m.group(5);
String time = m.group(6);
String info = null;
// and the type
int objectType = TYPE_OTHER;
switch (permissions.charAt(0)) {
case '-' :
objectType = TYPE_FILE;
break;
case 'b' :
objectType = TYPE_BLOCK;
break;
case 'c' :
objectType = TYPE_CHARACTER;
break;
case 'd' :
objectType = TYPE_DIRECTORY;
break;
case 'l' :
objectType = TYPE_LINK;
break;
case 's' :
objectType = TYPE_SOCKET;
break;
case 'p' :
objectType = TYPE_FIFO;
break;
}
// now check what we may be linking to
if (objectType == TYPE_LINK) {
String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$
// we should have 2 segments
if (segments.length == 2) {
// update the entry name to not contain the link
name = segments[0];
// and the link name
info = segments[1];
// now get the path to the link
String[] pathSegments = info.split(FILE_SEPARATOR);
if (pathSegments.length == 1) {
// the link is to something in the same directory,
// unless the link is ..
if ("..".equals(pathSegments[0])) { //$NON-NLS-1$
// set the type and we're done.
objectType = TYPE_DIRECTORY_LINK;
} else {
// either we found the object already
// or we'll find it later.
}
}
}
// add an arrow in front to specify it's a link.
info = "-> " + info; //$NON-NLS-1$;
}
// get the entry, either from an existing one, or a new one
FileEntry entry = getExistingEntry(name);
if (entry == null) {
entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */);
}
// add some misc info
entry.permissions = permissions;
entry.size = size;
entry.date = date;
entry.time = time;
entry.owner = owner;
entry.group = group;
if (objectType == TYPE_LINK) {
entry.info = info;
}
mEntryList.add(entry);
}
}
/**
* Queries for an already existing Entry per name
* @param name the name of the entry
* @return the existing FileEntry or null if no entry with a matching
* name exists.
*/
private FileEntry getExistingEntry(String name) {
for (int i = 0 ; i < mCurrentChildren.length; i++) {
FileEntry e = mCurrentChildren[i];
// since we're going to "erase" the one we use, we need to
// check that the item is not null.
if (e != null) {
// compare per name, case-sensitive.
if (name.equals(e.name)) {
// erase from the list
mCurrentChildren[i] = null;
// and return the object
return e;
}
}
}
// couldn't find any matching object, return null
return null;
}
@Override
public boolean isCancelled() {
return false;
}
/**
* Determine if any symlinks in the <code entries> list are links-to-directories, and if so
* mark them as such. This allows us to traverse them properly later on.
*/
public void finishLinks(IDevice device, ArrayList<FileEntry> entries)
throws TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, IOException {
final int[] nLines = {0};
MultiLineReceiver receiver = new MultiLineReceiver() {
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
Matcher m = LS_LD_PATTERN.matcher(line);
if (m.matches()) {
nLines[0]++;
}
}
}
@Override
public boolean isCancelled() {
return false;
}
};
for (FileEntry entry : entries) {
if (entry.getType() != TYPE_LINK) continue;
// We simply need to determine whether the referent is a directory or not.
// We do this by running `ls -ld ${link}/`. If the referent exists and is a
// directory, we'll see the normal directory listing. Otherwise, we'll see an
// error of some sort.
nLines[0] = 0;
final String command = String.format("ls -l -d %s%s", entry.getFullEscapedPath(),
FILE_SEPARATOR);
device.executeShellCommand(command, receiver);
if (nLines[0] > 0) {
// We saw lines matching the directory pattern, so it's a directory!
entry.setType(TYPE_DIRECTORY_LINK);
}
}
}
}
/**
* Classes which implement this interface provide a method that deals with asynchronous
* result from <code>ls</code> command on the device.
*
* @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver)
*/
public interface IListingReceiver {
void setChildren(FileEntry entry, FileEntry[] children);
void refreshEntry(FileEntry entry);
}
/**
* Creates a File Listing Service for a specified {@link Device}.
* @param device The Device the service is connected to.
*/
FileListingService(Device device) {
mDevice = device;
}
/**
* Returns the root element.
* @return the {@link FileEntry} object representing the root element or
* <code>null</code> if the device is invalid.
*/
public FileEntry getRoot() {
if (mDevice != null) {
if (mRoot == null) {
mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY,
true /* isRoot */);
}
return mRoot;
}
return null;
}
/**
* Returns the children of a {@link FileEntry}.
* <p/>
* This method supports a cache mechanism and synchronous and asynchronous modes.
* <p/>
* If <var>receiver</var> is <code>null</code>, the device side <code>ls</code>
* command is done synchronously, and the method will return upon completion of the command.<br>
* If <var>receiver</var> is non <code>null</code>, the command is launched is a separate
* thread and upon completion, the receiver will be notified of the result.
* <p/>
* The result for each <code>ls</code> command is cached in the parent
* <code>FileEntry</code>. <var>useCache</var> allows usage of this cache, but only if the
* cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms.
* After that a new <code>ls</code> command is always executed.
* <p/>
* If the cache is valid and <code>useCache == true</code>, the method will always simply
* return the value of the cache, whether a {@link IListingReceiver} has been provided or not.
*
* @param entry The parent entry.
* @param useCache A flag to use the cache or to force a new ls command.
* @param receiver A receiver for asynchronous calls.
* @return The list of children or <code>null</code> for asynchronous calls.
*
* @see FileEntry#getCachedChildren()
*/
public FileEntry[] getChildren(final FileEntry entry, boolean useCache,
final IListingReceiver receiver) {
// first thing we do is check the cache, and if we already have a recent
// enough children list, we just return that.
if (useCache && !entry.needFetch()) {
return entry.getCachedChildren();
}
// if there's no receiver, then this is a synchronous call, and we
// return the result of ls
if (receiver == null) {
doLs(entry);
return entry.getCachedChildren();
}
// this is a asynchronous call.
// we launch a thread that will do ls and give the listing
// to the receiver
Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$
@Override
public void run() {
doLs(entry);
receiver.setChildren(entry, entry.getCachedChildren());
final FileEntry[] children = entry.getCachedChildren();
if (children.length > 0 && children[0].isApplicationPackage()) {
final HashMap<String, FileEntry> map = new HashMap<String, FileEntry>();
for (FileEntry child : children) {
String path = child.getFullPath();
map.put(path, child);
}
// call pm.
String command = PM_FULL_LISTING;
try {
mDevice.executeShellCommand(command, new MultiLineReceiver() {
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
if (!line.isEmpty()) {
// get the filepath and package from the line
Matcher m = sPmPattern.matcher(line);
if (m.matches()) {
// get the children with that path
FileEntry entry = map.get(m.group(1));
if (entry != null) {
entry.info = m.group(2);
receiver.refreshEntry(entry);
}
}
}
}
}
@Override
public boolean isCancelled() {
return false;
}
});
} catch (Exception e) {
// adb failed somehow, we do nothing.
}
}
// if another thread is pending, launch it
synchronized (mThreadList) {
// first remove ourselves from the list
mThreadList.remove(this);
// then launch the next one if applicable.
if (!mThreadList.isEmpty()) {
Thread t = mThreadList.get(0);
t.start();
}
}
}
};
// we don't want to run multiple ls on the device at the same time, so we
// store the thread in a list and launch it only if there's no other thread running.
// the thread will launch the next one once it's done.
synchronized (mThreadList) {
// add to the list
mThreadList.add(t);
// if it's the only one, launch it.
if (mThreadList.size() == 1) {
t.start();
}
}
// and we return null.
return null;
}
/**
* Returns the children of a {@link FileEntry}.
* <p/>
* This method is the explicit synchronous version of
* {@link #getChildren(FileEntry, boolean, IListingReceiver)}. It is roughly equivalent to
* calling
* getChildren(FileEntry, false, null)
*
* @param entry The parent entry.
* @return The list of children
* @throws TimeoutException in case of timeout on the connection when sending the command.
* @throws AdbCommandRejectedException if adb rejects the command.
* @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output
* for a period longer than <var>maxTimeToOutputResponse</var>.
* @throws IOException in case of I/O error on the connection.
*/
public FileEntry[] getChildrenSync(final FileEntry entry) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
doLsAndThrow(entry);
return entry.getCachedChildren();
}
private void doLs(FileEntry entry) {
try {
doLsAndThrow(entry);
} catch (Exception e) {
// do nothing
}
}
private void doLsAndThrow(FileEntry entry) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
// create a list that will receive the list of the entries
ArrayList<FileEntry> entryList = new ArrayList<FileEntry>();
// create a list that will receive the link to compute post ls;
ArrayList<String> linkList = new ArrayList<String>();
try {
// create the command
String command = "ls -l " + entry.getFullEscapedPath(); //$NON-NLS-1$
if (entry.isDirectory()) {
// If we expect a file to behave like a directory, we should stick a "/" at the end.
// This is a good habit, and is mandatory for symlinks-to-directories, which will
// otherwise behave like symlinks.
command += FILE_SEPARATOR;
}
// create the receiver object that will parse the result from ls
LsReceiver receiver = new LsReceiver(entry, entryList, linkList);
// call ls.
mDevice.executeShellCommand(command, receiver);
// finish the process of the receiver to handle links
receiver.finishLinks(mDevice, entryList);
} finally {
// at this point we need to refresh the viewer
entry.fetchTime = System.currentTimeMillis();
// sort the children and set them as the new children
Collections.sort(entryList, FileEntry.sEntryComparator);
entry.setChildren(entryList);
}
}
}