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 17 package com.android.ddmlib; 18 19 import java.util.ArrayList; 20 import java.util.Collections; 21 import java.util.Comparator; 22 import java.util.HashMap; 23 import java.util.regex.Matcher; 24 import java.util.regex.Pattern; 25 26 /** 27 * Provides {@link Device} side file listing service. 28 * <p/>To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}. 29 */ 30 public final class FileListingService { 31 32 /** Pattern to find filenames that match "*.apk" */ 33 private final static Pattern sApkPattern = 34 Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ 35 36 private final static String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$ 37 38 /** Pattern to parse the output of the 'pm -lf' command.<br> 39 * The output format looks like:<br> 40 * /data/app/myapp.apk=com.mypackage.myapp */ 41 private final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$ 42 43 /** Top level data folder. */ 44 public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$ 45 /** Top level sdcard folder. */ 46 public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$ 47 /** Top level mount folder. */ 48 public final static String DIRECTORY_MNT = "mnt"; //$NON-NLS-1$ 49 /** Top level system folder. */ 50 public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$ 51 /** Top level temp folder. */ 52 public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$ 53 /** Application folder. */ 54 public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$ 55 56 private final static String[] sRootLevelApprovedItems = { 57 DIRECTORY_DATA, 58 DIRECTORY_SDCARD, 59 DIRECTORY_SYSTEM, 60 DIRECTORY_TEMP, 61 DIRECTORY_MNT, 62 }; 63 64 public static final long REFRESH_RATE = 5000L; 65 /** 66 * Refresh test has to be slightly lower for precision issue. 67 */ 68 static final long REFRESH_TEST = (long)(REFRESH_RATE * .8); 69 70 /** Entry type: File */ 71 public static final int TYPE_FILE = 0; 72 /** Entry type: Directory */ 73 public static final int TYPE_DIRECTORY = 1; 74 /** Entry type: Directory Link */ 75 public static final int TYPE_DIRECTORY_LINK = 2; 76 /** Entry type: Block */ 77 public static final int TYPE_BLOCK = 3; 78 /** Entry type: Character */ 79 public static final int TYPE_CHARACTER = 4; 80 /** Entry type: Link */ 81 public static final int TYPE_LINK = 5; 82 /** Entry type: Socket */ 83 public static final int TYPE_SOCKET = 6; 84 /** Entry type: FIFO */ 85 public static final int TYPE_FIFO = 7; 86 /** Entry type: Other */ 87 public static final int TYPE_OTHER = 8; 88 89 /** Device side file separator. */ 90 public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$ 91 92 private static final String FILE_ROOT = "/"; //$NON-NLS-1$ 93 94 95 /** 96 * Regexp pattern to parse the result from ls. 97 */ 98 private static Pattern sLsPattern = Pattern.compile( 99 "^([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$ 100 101 private Device mDevice; 102 private FileEntry mRoot; 103 104 private ArrayList<Thread> mThreadList = new ArrayList<Thread>(); 105 106 /** 107 * Represents an entry in a directory. This can be a file or a directory. 108 */ 109 public final static class FileEntry { 110 /** Pattern to escape filenames for shell command consumption. */ 111 private final static Pattern sEscapePattern = Pattern.compile( 112 "([\\\\()*+?\"'#/\\s])"); //$NON-NLS-1$ 113 114 /** 115 * Comparator object for FileEntry 116 */ 117 private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() { 118 public int compare(FileEntry o1, FileEntry o2) { 119 if (o1 instanceof FileEntry && o2 instanceof FileEntry) { 120 FileEntry fe1 = (FileEntry)o1; 121 FileEntry fe2 = (FileEntry)o2; 122 return fe1.name.compareTo(fe2.name); 123 } 124 return 0; 125 } 126 }; 127 128 FileEntry parent; 129 String name; 130 String info; 131 String permissions; 132 String size; 133 String date; 134 String time; 135 String owner; 136 String group; 137 int type; 138 boolean isAppPackage; 139 140 boolean isRoot; 141 142 /** 143 * Indicates whether the entry content has been fetched yet, or not. 144 */ 145 long fetchTime = 0; 146 147 final ArrayList<FileEntry> mChildren = new ArrayList<FileEntry>(); 148 149 /** 150 * Creates a new file entry. 151 * @param parent parent entry or null if entry is root 152 * @param name name of the entry. 153 * @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE}, 154 * {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}. 155 */ FileEntry(FileEntry parent, String name, int type, boolean isRoot)156 private FileEntry(FileEntry parent, String name, int type, boolean isRoot) { 157 this.parent = parent; 158 this.name = name; 159 this.type = type; 160 this.isRoot = isRoot; 161 162 checkAppPackageStatus(); 163 } 164 165 /** 166 * Returns the name of the entry 167 */ getName()168 public String getName() { 169 return name; 170 } 171 172 /** 173 * Returns the size string of the entry, as returned by <code>ls</code>. 174 */ getSize()175 public String getSize() { 176 return size; 177 } 178 179 /** 180 * Returns the size of the entry. 181 */ getSizeValue()182 public int getSizeValue() { 183 return Integer.parseInt(size); 184 } 185 186 /** 187 * Returns the date string of the entry, as returned by <code>ls</code>. 188 */ getDate()189 public String getDate() { 190 return date; 191 } 192 193 /** 194 * Returns the time string of the entry, as returned by <code>ls</code>. 195 */ getTime()196 public String getTime() { 197 return time; 198 } 199 200 /** 201 * Returns the permission string of the entry, as returned by <code>ls</code>. 202 */ getPermissions()203 public String getPermissions() { 204 return permissions; 205 } 206 207 /** 208 * Returns the extra info for the entry. 209 * <p/>For a link, it will be a description of the link. 210 * <p/>For an application apk file it will be the application package as returned 211 * by the Package Manager. 212 */ getInfo()213 public String getInfo() { 214 return info; 215 } 216 217 /** 218 * Return the full path of the entry. 219 * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator. 220 */ getFullPath()221 public String getFullPath() { 222 if (isRoot) { 223 return FILE_ROOT; 224 } 225 StringBuilder pathBuilder = new StringBuilder(); 226 fillPathBuilder(pathBuilder, false); 227 228 return pathBuilder.toString(); 229 } 230 231 /** 232 * Return the fully escaped path of the entry. This path is safe to use in a 233 * shell command line. 234 * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator 235 */ getFullEscapedPath()236 public String getFullEscapedPath() { 237 StringBuilder pathBuilder = new StringBuilder(); 238 fillPathBuilder(pathBuilder, true); 239 240 return pathBuilder.toString(); 241 } 242 243 /** 244 * Returns the path as a list of segments. 245 */ getPathSegments()246 public String[] getPathSegments() { 247 ArrayList<String> list = new ArrayList<String>(); 248 fillPathSegments(list); 249 250 return list.toArray(new String[list.size()]); 251 } 252 253 /** 254 * Returns true if the entry is a directory, false otherwise; 255 */ getType()256 public int getType() { 257 return type; 258 } 259 260 /** 261 * Returns if the entry is a folder or a link to a folder. 262 */ isDirectory()263 public boolean isDirectory() { 264 return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK; 265 } 266 267 /** 268 * Returns the parent entry. 269 */ getParent()270 public FileEntry getParent() { 271 return parent; 272 } 273 274 /** 275 * Returns the cached children of the entry. This returns the cache created from calling 276 * <code>FileListingService.getChildren()</code>. 277 */ getCachedChildren()278 public FileEntry[] getCachedChildren() { 279 return mChildren.toArray(new FileEntry[mChildren.size()]); 280 } 281 282 /** 283 * Returns the child {@link FileEntry} matching the name. 284 * This uses the cached children list. 285 * @param name the name of the child to return. 286 * @return the FileEntry matching the name or null. 287 */ findChild(String name)288 public FileEntry findChild(String name) { 289 for (FileEntry entry : mChildren) { 290 if (entry.name.equals(name)) { 291 return entry; 292 } 293 } 294 return null; 295 } 296 297 /** 298 * Returns whether the entry is the root. 299 */ isRoot()300 public boolean isRoot() { 301 return isRoot; 302 } 303 addChild(FileEntry child)304 void addChild(FileEntry child) { 305 mChildren.add(child); 306 } 307 setChildren(ArrayList<FileEntry> newChildren)308 void setChildren(ArrayList<FileEntry> newChildren) { 309 mChildren.clear(); 310 mChildren.addAll(newChildren); 311 } 312 needFetch()313 boolean needFetch() { 314 if (fetchTime == 0) { 315 return true; 316 } 317 long current = System.currentTimeMillis(); 318 if (current-fetchTime > REFRESH_TEST) { 319 return true; 320 } 321 322 return false; 323 } 324 325 /** 326 * Returns if the entry is a valid application package. 327 */ isApplicationPackage()328 public boolean isApplicationPackage() { 329 return isAppPackage; 330 } 331 332 /** 333 * Returns if the file name is an application package name. 334 */ isAppFileName()335 public boolean isAppFileName() { 336 Matcher m = sApkPattern.matcher(name); 337 return m.matches(); 338 } 339 340 /** 341 * Recursively fills the pathBuilder with the full path 342 * @param pathBuilder a StringBuilder used to create the path. 343 * @param escapePath Whether the path need to be escaped for consumption by 344 * a shell command line. 345 */ fillPathBuilder(StringBuilder pathBuilder, boolean escapePath)346 protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) { 347 if (isRoot) { 348 return; 349 } 350 351 if (parent != null) { 352 parent.fillPathBuilder(pathBuilder, escapePath); 353 } 354 pathBuilder.append(FILE_SEPARATOR); 355 pathBuilder.append(escapePath ? escape(name) : name); 356 } 357 358 /** 359 * Recursively fills the segment list with the full path. 360 * @param list The list of segments to fill. 361 */ fillPathSegments(ArrayList<String> list)362 protected void fillPathSegments(ArrayList<String> list) { 363 if (isRoot) { 364 return; 365 } 366 367 if (parent != null) { 368 parent.fillPathSegments(list); 369 } 370 371 list.add(name); 372 } 373 374 /** 375 * Sets the internal app package status flag. This checks whether the entry is in an app 376 * directory like /data/app or /system/app 377 */ checkAppPackageStatus()378 private void checkAppPackageStatus() { 379 isAppPackage = false; 380 381 String[] segments = getPathSegments(); 382 if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) { 383 isAppPackage = DIRECTORY_APP.equals(segments[1]) && 384 (DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0])); 385 } 386 } 387 388 /** 389 * Returns an escaped version of the entry name. 390 * @param entryName 391 */ escape(String entryName)392 private String escape(String entryName) { 393 return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$ 394 } 395 } 396 397 private class LsReceiver extends MultiLineReceiver { 398 399 private ArrayList<FileEntry> mEntryList; 400 private ArrayList<String> mLinkList; 401 private FileEntry[] mCurrentChildren; 402 private FileEntry mParentEntry; 403 404 /** 405 * Create an ls receiver/parser. 406 * @param currentChildren The list of current children. To prevent 407 * collapse during update, reusing the same FileEntry objects for 408 * files that were already there is paramount. 409 * @param entryList the list of new children to be filled by the 410 * receiver. 411 * @param linkList the list of link path to compute post ls, to figure 412 * out if the link pointed to a file or to a directory. 413 */ LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList, ArrayList<String> linkList)414 public LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList, 415 ArrayList<String> linkList) { 416 mParentEntry = parentEntry; 417 mCurrentChildren = parentEntry.getCachedChildren(); 418 mEntryList = entryList; 419 mLinkList = linkList; 420 } 421 422 @Override processNewLines(String[] lines)423 public void processNewLines(String[] lines) { 424 for (String line : lines) { 425 // no need to handle empty lines. 426 if (line.length() == 0) { 427 continue; 428 } 429 430 // run the line through the regexp 431 Matcher m = sLsPattern.matcher(line); 432 if (m.matches() == false) { 433 continue; 434 } 435 436 // get the name 437 String name = m.group(7); 438 439 // if the parent is root, we only accept selected items 440 if (mParentEntry.isRoot()) { 441 boolean found = false; 442 for (String approved : sRootLevelApprovedItems) { 443 if (approved.equals(name)) { 444 found = true; 445 break; 446 } 447 } 448 449 // if it's not in the approved list we skip this entry. 450 if (found == false) { 451 continue; 452 } 453 } 454 455 // get the rest of the groups 456 String permissions = m.group(1); 457 String owner = m.group(2); 458 String group = m.group(3); 459 String size = m.group(4); 460 String date = m.group(5); 461 String time = m.group(6); 462 String info = null; 463 464 // and the type 465 int objectType = TYPE_OTHER; 466 switch (permissions.charAt(0)) { 467 case '-' : 468 objectType = TYPE_FILE; 469 break; 470 case 'b' : 471 objectType = TYPE_BLOCK; 472 break; 473 case 'c' : 474 objectType = TYPE_CHARACTER; 475 break; 476 case 'd' : 477 objectType = TYPE_DIRECTORY; 478 break; 479 case 'l' : 480 objectType = TYPE_LINK; 481 break; 482 case 's' : 483 objectType = TYPE_SOCKET; 484 break; 485 case 'p' : 486 objectType = TYPE_FIFO; 487 break; 488 } 489 490 491 // now check what we may be linking to 492 if (objectType == TYPE_LINK) { 493 String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$ 494 495 // we should have 2 segments 496 if (segments.length == 2) { 497 // update the entry name to not contain the link 498 name = segments[0]; 499 500 // and the link name 501 info = segments[1]; 502 503 // now get the path to the link 504 String[] pathSegments = info.split(FILE_SEPARATOR); 505 if (pathSegments.length == 1) { 506 // the link is to something in the same directory, 507 // unless the link is .. 508 if ("..".equals(pathSegments[0])) { //$NON-NLS-1$ 509 // set the type and we're done. 510 objectType = TYPE_DIRECTORY_LINK; 511 } else { 512 // either we found the object already 513 // or we'll find it later. 514 } 515 } 516 } 517 518 // add an arrow in front to specify it's a link. 519 info = "-> " + info; //$NON-NLS-1$; 520 } 521 522 // get the entry, either from an existing one, or a new one 523 FileEntry entry = getExistingEntry(name); 524 if (entry == null) { 525 entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */); 526 } 527 528 // add some misc info 529 entry.permissions = permissions; 530 entry.size = size; 531 entry.date = date; 532 entry.time = time; 533 entry.owner = owner; 534 entry.group = group; 535 if (objectType == TYPE_LINK) { 536 entry.info = info; 537 } 538 539 mEntryList.add(entry); 540 } 541 } 542 543 /** 544 * Queries for an already existing Entry per name 545 * @param name the name of the entry 546 * @return the existing FileEntry or null if no entry with a matching 547 * name exists. 548 */ getExistingEntry(String name)549 private FileEntry getExistingEntry(String name) { 550 for (int i = 0 ; i < mCurrentChildren.length; i++) { 551 FileEntry e = mCurrentChildren[i]; 552 553 // since we're going to "erase" the one we use, we need to 554 // check that the item is not null. 555 if (e != null) { 556 // compare per name, case-sensitive. 557 if (name.equals(e.name)) { 558 // erase from the list 559 mCurrentChildren[i] = null; 560 561 // and return the object 562 return e; 563 } 564 } 565 } 566 567 // couldn't find any matching object, return null 568 return null; 569 } 570 isCancelled()571 public boolean isCancelled() { 572 return false; 573 } 574 finishLinks()575 public void finishLinks() { 576 // TODO Handle links in the listing service 577 } 578 } 579 580 /** 581 * Classes which implement this interface provide a method that deals with asynchronous 582 * result from <code>ls</code> command on the device. 583 * 584 * @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver) 585 */ 586 public interface IListingReceiver { setChildren(FileEntry entry, FileEntry[] children)587 public void setChildren(FileEntry entry, FileEntry[] children); 588 refreshEntry(FileEntry entry)589 public void refreshEntry(FileEntry entry); 590 } 591 592 /** 593 * Creates a File Listing Service for a specified {@link Device}. 594 * @param device The Device the service is connected to. 595 */ FileListingService(Device device)596 FileListingService(Device device) { 597 mDevice = device; 598 } 599 600 /** 601 * Returns the root element. 602 * @return the {@link FileEntry} object representing the root element or 603 * <code>null</code> if the device is invalid. 604 */ getRoot()605 public FileEntry getRoot() { 606 if (mDevice != null) { 607 if (mRoot == null) { 608 mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY, 609 true /* isRoot */); 610 } 611 612 return mRoot; 613 } 614 615 return null; 616 } 617 618 /** 619 * Returns the children of a {@link FileEntry}. 620 * <p/> 621 * This method supports a cache mechanism and synchronous and asynchronous modes. 622 * <p/> 623 * If <var>receiver</var> is <code>null</code>, the device side <code>ls</code> 624 * command is done synchronously, and the method will return upon completion of the command.<br> 625 * If <var>receiver</var> is non <code>null</code>, the command is launched is a separate 626 * thread and upon completion, the receiver will be notified of the result. 627 * <p/> 628 * The result for each <code>ls</code> command is cached in the parent 629 * <code>FileEntry</code>. <var>useCache</var> allows usage of this cache, but only if the 630 * cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms. 631 * After that a new <code>ls</code> command is always executed. 632 * <p/> 633 * If the cache is valid and <code>useCache == true</code>, the method will always simply 634 * return the value of the cache, whether a {@link IListingReceiver} has been provided or not. 635 * 636 * @param entry The parent entry. 637 * @param useCache A flag to use the cache or to force a new ls command. 638 * @param receiver A receiver for asynchronous calls. 639 * @return The list of children or <code>null</code> for asynchronous calls. 640 * 641 * @see FileEntry#getCachedChildren() 642 */ getChildren(final FileEntry entry, boolean useCache, final IListingReceiver receiver)643 public FileEntry[] getChildren(final FileEntry entry, boolean useCache, 644 final IListingReceiver receiver) { 645 // first thing we do is check the cache, and if we already have a recent 646 // enough children list, we just return that. 647 if (useCache && entry.needFetch() == false) { 648 return entry.getCachedChildren(); 649 } 650 651 // if there's no receiver, then this is a synchronous call, and we 652 // return the result of ls 653 if (receiver == null) { 654 doLs(entry); 655 return entry.getCachedChildren(); 656 } 657 658 // this is a asynchronous call. 659 // we launch a thread that will do ls and give the listing 660 // to the receiver 661 Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$ 662 @Override 663 public void run() { 664 doLs(entry); 665 666 receiver.setChildren(entry, entry.getCachedChildren()); 667 668 final FileEntry[] children = entry.getCachedChildren(); 669 if (children.length > 0 && children[0].isApplicationPackage()) { 670 final HashMap<String, FileEntry> map = new HashMap<String, FileEntry>(); 671 672 for (FileEntry child : children) { 673 String path = child.getFullPath(); 674 map.put(path, child); 675 } 676 677 // call pm. 678 String command = PM_FULL_LISTING; 679 try { 680 mDevice.executeShellCommand(command, new MultiLineReceiver() { 681 @Override 682 public void processNewLines(String[] lines) { 683 for (String line : lines) { 684 if (line.length() > 0) { 685 // get the filepath and package from the line 686 Matcher m = sPmPattern.matcher(line); 687 if (m.matches()) { 688 // get the children with that path 689 FileEntry entry = map.get(m.group(1)); 690 if (entry != null) { 691 entry.info = m.group(2); 692 receiver.refreshEntry(entry); 693 } 694 } 695 } 696 } 697 } 698 public boolean isCancelled() { 699 return false; 700 } 701 }); 702 } catch (Exception e) { 703 // adb failed somehow, we do nothing. 704 } 705 } 706 707 708 // if another thread is pending, launch it 709 synchronized (mThreadList) { 710 // first remove ourselves from the list 711 mThreadList.remove(this); 712 713 // then launch the next one if applicable. 714 if (mThreadList.size() > 0) { 715 Thread t = mThreadList.get(0); 716 t.start(); 717 } 718 } 719 } 720 }; 721 722 // we don't want to run multiple ls on the device at the same time, so we 723 // store the thread in a list and launch it only if there's no other thread running. 724 // the thread will launch the next one once it's done. 725 synchronized (mThreadList) { 726 // add to the list 727 mThreadList.add(t); 728 729 // if it's the only one, launch it. 730 if (mThreadList.size() == 1) { 731 t.start(); 732 } 733 } 734 735 // and we return null. 736 return null; 737 } 738 doLs(FileEntry entry)739 private void doLs(FileEntry entry) { 740 // create a list that will receive the list of the entries 741 ArrayList<FileEntry> entryList = new ArrayList<FileEntry>(); 742 743 // create a list that will receive the link to compute post ls; 744 ArrayList<String> linkList = new ArrayList<String>(); 745 746 try { 747 // create the command 748 String command = "ls -l " + entry.getFullPath(); //$NON-NLS-1$ 749 750 // create the receiver object that will parse the result from ls 751 LsReceiver receiver = new LsReceiver(entry, entryList, linkList); 752 753 // call ls. 754 mDevice.executeShellCommand(command, receiver); 755 756 // finish the process of the receiver to handle links 757 receiver.finishLinks(); 758 } catch (Exception e) { 759 // catch all and do nothing. 760 } 761 762 763 // at this point we need to refresh the viewer 764 entry.fetchTime = System.currentTimeMillis(); 765 766 // sort the children and set them as the new children 767 Collections.sort(entryList, FileEntry.sEntryComparator); 768 entry.setChildren(entryList); 769 } 770 } 771