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