• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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