com.android.ddmlib.FileListingService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ddmlib Show documentation
Show all versions of ddmlib Show documentation
Library providing APIs to talk to Android devices
/*
* 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.
* 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.
* The output format looks like:
* /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 mThreadList = new ArrayList();
/**
* 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 sEntryComparator = new Comparator() {
@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 mChildren = new ArrayList();
/**
* 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 ls
.
*/
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 ls
.
*/
public String getDate() {
return date;
}
/**
* Returns the time string of the entry, as returned by ls
.
*/
public String getTime() {
return time;
}
/**
* Returns the permission string of the entry, as returned by ls
.
*/
public String getPermissions() {
return permissions;
}
/**
* Returns the owner string of the entry, as returned by ls
.
*/
public String getOwner() {
return owner;
}
/**
* Returns the group owner of the entry, as returned by ls
.
*/
public String getGroup() {
return group;
}
/**
* Returns the extra info for the entry.
* For a link, it will be a description of the link.
* 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 list = new ArrayList();
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
* FileListingService.getChildren()
.
*/
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 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 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 mEntryList;
private ArrayList 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 entryList,
ArrayList 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 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 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 ls
command on the device.
*
* @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver)
*/
public interface IListingReceiver {
public void setChildren(FileEntry entry, FileEntry[] children);
public 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
* null
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}.
*
* This method supports a cache mechanism and synchronous and asynchronous modes.
*
* If receiver is null
, the device side ls
* command is done synchronously, and the method will return upon completion of the command.
* If receiver is non null
, the command is launched is a separate
* thread and upon completion, the receiver will be notified of the result.
*
* The result for each ls
command is cached in the parent
* FileEntry
. useCache 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 ls
command is always executed.
*
* If the cache is valid and useCache == true
, 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 null
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 map = new HashMap();
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}.
*
* 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 maxTimeToOutputResponse.
* @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 entryList = new ArrayList();
// create a list that will receive the link to compute post ls;
ArrayList linkList = new ArrayList();
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);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy