All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.ddmlib.FileListingService Maven / Gradle / Ivy

There is a newer version: 25.3.0
Show newest version
/*
 * 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