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