• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 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 android.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.provider.DocumentsContract.Document;
22 import android.system.ErrnoException;
23 import android.system.Os;
24 import android.system.StructStat;
25 import android.text.TextUtils;
26 import android.util.Log;
27 import android.util.Slog;
28 import android.webkit.MimeTypeMap;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 
32 import libcore.util.EmptyArray;
33 
34 import java.io.BufferedInputStream;
35 import java.io.ByteArrayOutputStream;
36 import java.io.File;
37 import java.io.FileDescriptor;
38 import java.io.FileInputStream;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.FilenameFilter;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.nio.charset.StandardCharsets;
45 import java.util.Arrays;
46 import java.util.Comparator;
47 import java.util.Objects;
48 import java.util.regex.Pattern;
49 import java.util.zip.CRC32;
50 import java.util.zip.CheckedInputStream;
51 
52 /**
53  * Tools for managing files.  Not for public consumption.
54  * @hide
55  */
56 public class FileUtils {
57     private static final String TAG = "FileUtils";
58 
59     public static final int S_IRWXU = 00700;
60     public static final int S_IRUSR = 00400;
61     public static final int S_IWUSR = 00200;
62     public static final int S_IXUSR = 00100;
63 
64     public static final int S_IRWXG = 00070;
65     public static final int S_IRGRP = 00040;
66     public static final int S_IWGRP = 00020;
67     public static final int S_IXGRP = 00010;
68 
69     public static final int S_IRWXO = 00007;
70     public static final int S_IROTH = 00004;
71     public static final int S_IWOTH = 00002;
72     public static final int S_IXOTH = 00001;
73 
74     /** Regular expression for safe filenames: no spaces or metacharacters.
75       *
76       * Use a preload holder so that FileUtils can be compile-time initialized.
77       */
78     private static class NoImagePreloadHolder {
79         public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
80     }
81 
82     private static final File[] EMPTY = new File[0];
83 
84     /**
85      * Set owner and mode of of given {@link File}.
86      *
87      * @param mode to apply through {@code chmod}
88      * @param uid to apply through {@code chown}, or -1 to leave unchanged
89      * @param gid to apply through {@code chown}, or -1 to leave unchanged
90      * @return 0 on success, otherwise errno.
91      */
setPermissions(File path, int mode, int uid, int gid)92     public static int setPermissions(File path, int mode, int uid, int gid) {
93         return setPermissions(path.getAbsolutePath(), mode, uid, gid);
94     }
95 
96     /**
97      * Set owner and mode of of given path.
98      *
99      * @param mode to apply through {@code chmod}
100      * @param uid to apply through {@code chown}, or -1 to leave unchanged
101      * @param gid to apply through {@code chown}, or -1 to leave unchanged
102      * @return 0 on success, otherwise errno.
103      */
setPermissions(String path, int mode, int uid, int gid)104     public static int setPermissions(String path, int mode, int uid, int gid) {
105         try {
106             Os.chmod(path, mode);
107         } catch (ErrnoException e) {
108             Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
109             return e.errno;
110         }
111 
112         if (uid >= 0 || gid >= 0) {
113             try {
114                 Os.chown(path, uid, gid);
115             } catch (ErrnoException e) {
116                 Slog.w(TAG, "Failed to chown(" + path + "): " + e);
117                 return e.errno;
118             }
119         }
120 
121         return 0;
122     }
123 
124     /**
125      * Set owner and mode of of given {@link FileDescriptor}.
126      *
127      * @param mode to apply through {@code chmod}
128      * @param uid to apply through {@code chown}, or -1 to leave unchanged
129      * @param gid to apply through {@code chown}, or -1 to leave unchanged
130      * @return 0 on success, otherwise errno.
131      */
setPermissions(FileDescriptor fd, int mode, int uid, int gid)132     public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) {
133         try {
134             Os.fchmod(fd, mode);
135         } catch (ErrnoException e) {
136             Slog.w(TAG, "Failed to fchmod(): " + e);
137             return e.errno;
138         }
139 
140         if (uid >= 0 || gid >= 0) {
141             try {
142                 Os.fchown(fd, uid, gid);
143             } catch (ErrnoException e) {
144                 Slog.w(TAG, "Failed to fchown(): " + e);
145                 return e.errno;
146             }
147         }
148 
149         return 0;
150     }
151 
copyPermissions(File from, File to)152     public static void copyPermissions(File from, File to) throws IOException {
153         try {
154             final StructStat stat = Os.stat(from.getAbsolutePath());
155             Os.chmod(to.getAbsolutePath(), stat.st_mode);
156             Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid);
157         } catch (ErrnoException e) {
158             throw e.rethrowAsIOException();
159         }
160     }
161 
162     /**
163      * Return owning UID of given path, otherwise -1.
164      */
getUid(String path)165     public static int getUid(String path) {
166         try {
167             return Os.stat(path).st_uid;
168         } catch (ErrnoException e) {
169             return -1;
170         }
171     }
172 
173     /**
174      * Perform an fsync on the given FileOutputStream.  The stream at this
175      * point must be flushed but not yet closed.
176      */
sync(FileOutputStream stream)177     public static boolean sync(FileOutputStream stream) {
178         try {
179             if (stream != null) {
180                 stream.getFD().sync();
181             }
182             return true;
183         } catch (IOException e) {
184         }
185         return false;
186     }
187 
188     @Deprecated
copyFile(File srcFile, File destFile)189     public static boolean copyFile(File srcFile, File destFile) {
190         try {
191             copyFileOrThrow(srcFile, destFile);
192             return true;
193         } catch (IOException e) {
194             return false;
195         }
196     }
197 
198     // copy a file from srcFile to destFile, return true if succeed, return
199     // false if fail
copyFileOrThrow(File srcFile, File destFile)200     public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
201         try (InputStream in = new FileInputStream(srcFile)) {
202             copyToFileOrThrow(in, destFile);
203         }
204     }
205 
206     @Deprecated
copyToFile(InputStream inputStream, File destFile)207     public static boolean copyToFile(InputStream inputStream, File destFile) {
208         try {
209             copyToFileOrThrow(inputStream, destFile);
210             return true;
211         } catch (IOException e) {
212             return false;
213         }
214     }
215 
216     /**
217      * Copy data from a source stream to destFile.
218      * Return true if succeed, return false if failed.
219      */
copyToFileOrThrow(InputStream inputStream, File destFile)220     public static void copyToFileOrThrow(InputStream inputStream, File destFile)
221             throws IOException {
222         if (destFile.exists()) {
223             destFile.delete();
224         }
225         FileOutputStream out = new FileOutputStream(destFile);
226         try {
227             byte[] buffer = new byte[4096];
228             int bytesRead;
229             while ((bytesRead = inputStream.read(buffer)) >= 0) {
230                 out.write(buffer, 0, bytesRead);
231             }
232         } finally {
233             out.flush();
234             try {
235                 out.getFD().sync();
236             } catch (IOException e) {
237             }
238             out.close();
239         }
240     }
241 
242     /**
243      * Check if a filename is "safe" (no metacharacters or spaces).
244      * @param file  The file to check
245      */
isFilenameSafe(File file)246     public static boolean isFilenameSafe(File file) {
247         // Note, we check whether it matches what's known to be safe,
248         // rather than what's known to be unsafe.  Non-ASCII, control
249         // characters, etc. are all unsafe by default.
250         return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
251     }
252 
253     /**
254      * Read a text file into a String, optionally limiting the length.
255      * @param file to read (will not seek, so things like /proc files are OK)
256      * @param max length (positive for head, negative of tail, 0 for no limit)
257      * @param ellipsis to add of the file was truncated (can be null)
258      * @return the contents of the file, possibly truncated
259      * @throws IOException if something goes wrong reading the file
260      */
readTextFile(File file, int max, String ellipsis)261     public static String readTextFile(File file, int max, String ellipsis) throws IOException {
262         InputStream input = new FileInputStream(file);
263         // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
264         // input stream, bytes read not equal to buffer size is not necessarily the correct
265         // indication for EOF; but it is true for BufferedInputStream due to its implementation.
266         BufferedInputStream bis = new BufferedInputStream(input);
267         try {
268             long size = file.length();
269             if (max > 0 || (size > 0 && max == 0)) {  // "head" mode: read the first N bytes
270                 if (size > 0 && (max == 0 || size < max)) max = (int) size;
271                 byte[] data = new byte[max + 1];
272                 int length = bis.read(data);
273                 if (length <= 0) return "";
274                 if (length <= max) return new String(data, 0, length);
275                 if (ellipsis == null) return new String(data, 0, max);
276                 return new String(data, 0, max) + ellipsis;
277             } else if (max < 0) {  // "tail" mode: keep the last N
278                 int len;
279                 boolean rolled = false;
280                 byte[] last = null;
281                 byte[] data = null;
282                 do {
283                     if (last != null) rolled = true;
284                     byte[] tmp = last; last = data; data = tmp;
285                     if (data == null) data = new byte[-max];
286                     len = bis.read(data);
287                 } while (len == data.length);
288 
289                 if (last == null && len <= 0) return "";
290                 if (last == null) return new String(data, 0, len);
291                 if (len > 0) {
292                     rolled = true;
293                     System.arraycopy(last, len, last, 0, last.length - len);
294                     System.arraycopy(data, 0, last, last.length - len, len);
295                 }
296                 if (ellipsis == null || !rolled) return new String(last);
297                 return ellipsis + new String(last);
298             } else {  // "cat" mode: size unknown, read it all in streaming fashion
299                 ByteArrayOutputStream contents = new ByteArrayOutputStream();
300                 int len;
301                 byte[] data = new byte[1024];
302                 do {
303                     len = bis.read(data);
304                     if (len > 0) contents.write(data, 0, len);
305                 } while (len == data.length);
306                 return contents.toString();
307             }
308         } finally {
309             bis.close();
310             input.close();
311         }
312     }
313 
stringToFile(File file, String string)314     public static void stringToFile(File file, String string) throws IOException {
315         stringToFile(file.getAbsolutePath(), string);
316     }
317 
318     /*
319      * Writes the bytes given in {@code content} to the file whose absolute path
320      * is {@code filename}.
321      */
bytesToFile(String filename, byte[] content)322     public static void bytesToFile(String filename, byte[] content) throws IOException {
323         try (FileOutputStream fos = new FileOutputStream(filename)) {
324             fos.write(content);
325         }
326     }
327 
328     /**
329      * Writes string to file. Basically same as "echo -n $string > $filename"
330      *
331      * @param filename
332      * @param string
333      * @throws IOException
334      */
stringToFile(String filename, String string)335     public static void stringToFile(String filename, String string) throws IOException {
336         bytesToFile(filename, string.getBytes(StandardCharsets.UTF_8));
337     }
338 
339     /**
340      * Computes the checksum of a file using the CRC32 checksum routine.
341      * The value of the checksum is returned.
342      *
343      * @param file  the file to checksum, must not be null
344      * @return the checksum value or an exception is thrown.
345      */
checksumCrc32(File file)346     public static long checksumCrc32(File file) throws FileNotFoundException, IOException {
347         CRC32 checkSummer = new CRC32();
348         CheckedInputStream cis = null;
349 
350         try {
351             cis = new CheckedInputStream( new FileInputStream(file), checkSummer);
352             byte[] buf = new byte[128];
353             while(cis.read(buf) >= 0) {
354                 // Just read for checksum to get calculated.
355             }
356             return checkSummer.getValue();
357         } finally {
358             if (cis != null) {
359                 try {
360                     cis.close();
361                 } catch (IOException e) {
362                 }
363             }
364         }
365     }
366 
367     /**
368      * Delete older files in a directory until only those matching the given
369      * constraints remain.
370      *
371      * @param minCount Always keep at least this many files.
372      * @param minAgeMs Always keep files younger than this age, in milliseconds.
373      * @return if any files were deleted.
374      */
deleteOlderFiles(File dir, int minCount, long minAgeMs)375     public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) {
376         if (minCount < 0 || minAgeMs < 0) {
377             throw new IllegalArgumentException("Constraints must be positive or 0");
378         }
379 
380         final File[] files = dir.listFiles();
381         if (files == null) return false;
382 
383         // Sort with newest files first
384         Arrays.sort(files, new Comparator<File>() {
385             @Override
386             public int compare(File lhs, File rhs) {
387                 return Long.compare(rhs.lastModified(), lhs.lastModified());
388             }
389         });
390 
391         // Keep at least minCount files
392         boolean deleted = false;
393         for (int i = minCount; i < files.length; i++) {
394             final File file = files[i];
395 
396             // Keep files newer than minAgeMs
397             final long age = System.currentTimeMillis() - file.lastModified();
398             if (age > minAgeMs) {
399                 if (file.delete()) {
400                     Log.d(TAG, "Deleted old file " + file);
401                     deleted = true;
402                 }
403             }
404         }
405         return deleted;
406     }
407 
408     /**
409      * Test if a file lives under the given directory, either as a direct child
410      * or a distant grandchild.
411      * <p>
412      * Both files <em>must</em> have been resolved using
413      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
414      * attacks.
415      */
contains(File[] dirs, File file)416     public static boolean contains(File[] dirs, File file) {
417         for (File dir : dirs) {
418             if (contains(dir, file)) {
419                 return true;
420             }
421         }
422         return false;
423     }
424 
425     /**
426      * Test if a file lives under the given directory, either as a direct child
427      * or a distant grandchild.
428      * <p>
429      * Both files <em>must</em> have been resolved using
430      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
431      * attacks.
432      */
contains(File dir, File file)433     public static boolean contains(File dir, File file) {
434         if (dir == null || file == null) return false;
435         return contains(dir.getAbsolutePath(), file.getAbsolutePath());
436     }
437 
contains(String dirPath, String filePath)438     public static boolean contains(String dirPath, String filePath) {
439         if (dirPath.equals(filePath)) {
440             return true;
441         }
442         if (!dirPath.endsWith("/")) {
443             dirPath += "/";
444         }
445         return filePath.startsWith(dirPath);
446     }
447 
deleteContentsAndDir(File dir)448     public static boolean deleteContentsAndDir(File dir) {
449         if (deleteContents(dir)) {
450             return dir.delete();
451         } else {
452             return false;
453         }
454     }
455 
deleteContents(File dir)456     public static boolean deleteContents(File dir) {
457         File[] files = dir.listFiles();
458         boolean success = true;
459         if (files != null) {
460             for (File file : files) {
461                 if (file.isDirectory()) {
462                     success &= deleteContents(file);
463                 }
464                 if (!file.delete()) {
465                     Log.w(TAG, "Failed to delete " + file);
466                     success = false;
467                 }
468             }
469         }
470         return success;
471     }
472 
isValidExtFilenameChar(char c)473     private static boolean isValidExtFilenameChar(char c) {
474         switch (c) {
475             case '\0':
476             case '/':
477                 return false;
478             default:
479                 return true;
480         }
481     }
482 
483     /**
484      * Check if given filename is valid for an ext4 filesystem.
485      */
isValidExtFilename(String name)486     public static boolean isValidExtFilename(String name) {
487         return (name != null) && name.equals(buildValidExtFilename(name));
488     }
489 
490     /**
491      * Mutate the given filename to make it valid for an ext4 filesystem,
492      * replacing any invalid characters with "_".
493      */
buildValidExtFilename(String name)494     public static String buildValidExtFilename(String name) {
495         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
496             return "(invalid)";
497         }
498         final StringBuilder res = new StringBuilder(name.length());
499         for (int i = 0; i < name.length(); i++) {
500             final char c = name.charAt(i);
501             if (isValidExtFilenameChar(c)) {
502                 res.append(c);
503             } else {
504                 res.append('_');
505             }
506         }
507         trimFilename(res, 255);
508         return res.toString();
509     }
510 
isValidFatFilenameChar(char c)511     private static boolean isValidFatFilenameChar(char c) {
512         if ((0x00 <= c && c <= 0x1f)) {
513             return false;
514         }
515         switch (c) {
516             case '"':
517             case '*':
518             case '/':
519             case ':':
520             case '<':
521             case '>':
522             case '?':
523             case '\\':
524             case '|':
525             case 0x7F:
526                 return false;
527             default:
528                 return true;
529         }
530     }
531 
532     /**
533      * Check if given filename is valid for a FAT filesystem.
534      */
isValidFatFilename(String name)535     public static boolean isValidFatFilename(String name) {
536         return (name != null) && name.equals(buildValidFatFilename(name));
537     }
538 
539     /**
540      * Mutate the given filename to make it valid for a FAT filesystem,
541      * replacing any invalid characters with "_".
542      */
buildValidFatFilename(String name)543     public static String buildValidFatFilename(String name) {
544         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
545             return "(invalid)";
546         }
547         final StringBuilder res = new StringBuilder(name.length());
548         for (int i = 0; i < name.length(); i++) {
549             final char c = name.charAt(i);
550             if (isValidFatFilenameChar(c)) {
551                 res.append(c);
552             } else {
553                 res.append('_');
554             }
555         }
556         // Even though vfat allows 255 UCS-2 chars, we might eventually write to
557         // ext4 through a FUSE layer, so use that limit.
558         trimFilename(res, 255);
559         return res.toString();
560     }
561 
562     @VisibleForTesting
trimFilename(String str, int maxBytes)563     public static String trimFilename(String str, int maxBytes) {
564         final StringBuilder res = new StringBuilder(str);
565         trimFilename(res, maxBytes);
566         return res.toString();
567     }
568 
trimFilename(StringBuilder res, int maxBytes)569     private static void trimFilename(StringBuilder res, int maxBytes) {
570         byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
571         if (raw.length > maxBytes) {
572             maxBytes -= 3;
573             while (raw.length > maxBytes) {
574                 res.deleteCharAt(res.length() / 2);
575                 raw = res.toString().getBytes(StandardCharsets.UTF_8);
576             }
577             res.insert(res.length() / 2, "...");
578         }
579     }
580 
rewriteAfterRename(File beforeDir, File afterDir, String path)581     public static String rewriteAfterRename(File beforeDir, File afterDir, String path) {
582         if (path == null) return null;
583         final File result = rewriteAfterRename(beforeDir, afterDir, new File(path));
584         return (result != null) ? result.getAbsolutePath() : null;
585     }
586 
rewriteAfterRename(File beforeDir, File afterDir, String[] paths)587     public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) {
588         if (paths == null) return null;
589         final String[] result = new String[paths.length];
590         for (int i = 0; i < paths.length; i++) {
591             result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]);
592         }
593         return result;
594     }
595 
596     /**
597      * Given a path under the "before" directory, rewrite it to live under the
598      * "after" directory. For example, {@code /before/foo/bar.txt} would become
599      * {@code /after/foo/bar.txt}.
600      */
rewriteAfterRename(File beforeDir, File afterDir, File file)601     public static File rewriteAfterRename(File beforeDir, File afterDir, File file) {
602         if (file == null || beforeDir == null || afterDir == null) return null;
603         if (contains(beforeDir, file)) {
604             final String splice = file.getAbsolutePath().substring(
605                     beforeDir.getAbsolutePath().length());
606             return new File(afterDir, splice);
607         }
608         return null;
609     }
610 
buildUniqueFileWithExtension(File parent, String name, String ext)611     private static File buildUniqueFileWithExtension(File parent, String name, String ext)
612             throws FileNotFoundException {
613         File file = buildFile(parent, name, ext);
614 
615         // If conflicting file, try adding counter suffix
616         int n = 0;
617         while (file.exists()) {
618             if (n++ >= 32) {
619                 throw new FileNotFoundException("Failed to create unique file");
620             }
621             file = buildFile(parent, name + " (" + n + ")", ext);
622         }
623 
624         return file;
625     }
626 
627     /**
628      * Generates a unique file name under the given parent directory. If the display name doesn't
629      * have an extension that matches the requested MIME type, the default extension for that MIME
630      * type is appended. If a file already exists, the name is appended with a numerical value to
631      * make it unique.
632      *
633      * For example, the display name 'example' with 'text/plain' MIME might produce
634      * 'example.txt' or 'example (1).txt', etc.
635      *
636      * @throws FileNotFoundException
637      */
buildUniqueFile(File parent, String mimeType, String displayName)638     public static File buildUniqueFile(File parent, String mimeType, String displayName)
639             throws FileNotFoundException {
640         final String[] parts = splitFileName(mimeType, displayName);
641         return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
642     }
643 
644     /**
645      * Generates a unique file name under the given parent directory, keeping
646      * any extension intact.
647      */
buildUniqueFile(File parent, String displayName)648     public static File buildUniqueFile(File parent, String displayName)
649             throws FileNotFoundException {
650         final String name;
651         final String ext;
652 
653         // Extract requested extension from display name
654         final int lastDot = displayName.lastIndexOf('.');
655         if (lastDot >= 0) {
656             name = displayName.substring(0, lastDot);
657             ext = displayName.substring(lastDot + 1);
658         } else {
659             name = displayName;
660             ext = null;
661         }
662 
663         return buildUniqueFileWithExtension(parent, name, ext);
664     }
665 
666     /**
667      * Splits file name into base name and extension.
668      * If the display name doesn't have an extension that matches the requested MIME type, the
669      * extension is regarded as a part of filename and default extension for that MIME type is
670      * appended.
671      */
splitFileName(String mimeType, String displayName)672     public static String[] splitFileName(String mimeType, String displayName) {
673         String name;
674         String ext;
675 
676         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
677             name = displayName;
678             ext = null;
679         } else {
680             String mimeTypeFromExt;
681 
682             // Extract requested extension from display name
683             final int lastDot = displayName.lastIndexOf('.');
684             if (lastDot >= 0) {
685                 name = displayName.substring(0, lastDot);
686                 ext = displayName.substring(lastDot + 1);
687                 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
688                         ext.toLowerCase());
689             } else {
690                 name = displayName;
691                 ext = null;
692                 mimeTypeFromExt = null;
693             }
694 
695             if (mimeTypeFromExt == null) {
696                 mimeTypeFromExt = "application/octet-stream";
697             }
698 
699             final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
700                     mimeType);
701             if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) {
702                 // Extension maps back to requested MIME type; allow it
703             } else {
704                 // No match; insist that create file matches requested MIME
705                 name = displayName;
706                 ext = extFromMimeType;
707             }
708         }
709 
710         if (ext == null) {
711             ext = "";
712         }
713 
714         return new String[] { name, ext };
715     }
716 
buildFile(File parent, String name, String ext)717     private static File buildFile(File parent, String name, String ext) {
718         if (TextUtils.isEmpty(ext)) {
719             return new File(parent, name);
720         } else {
721             return new File(parent, name + "." + ext);
722         }
723     }
724 
listOrEmpty(@ullable File dir)725     public static @NonNull String[] listOrEmpty(@Nullable File dir) {
726         if (dir == null) return EmptyArray.STRING;
727         final String[] res = dir.list();
728         if (res != null) {
729             return res;
730         } else {
731             return EmptyArray.STRING;
732         }
733     }
734 
listFilesOrEmpty(@ullable File dir)735     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
736         if (dir == null) return EMPTY;
737         final File[] res = dir.listFiles();
738         if (res != null) {
739             return res;
740         } else {
741             return EMPTY;
742         }
743     }
744 
listFilesOrEmpty(@ullable File dir, FilenameFilter filter)745     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) {
746         if (dir == null) return EMPTY;
747         final File[] res = dir.listFiles(filter);
748         if (res != null) {
749             return res;
750         } else {
751             return EMPTY;
752         }
753     }
754 
newFileOrNull(@ullable String path)755     public static @Nullable File newFileOrNull(@Nullable String path) {
756         return (path != null) ? new File(path) : null;
757     }
758 
759     /**
760      * Creates a directory with name {@code name} under an existing directory {@code baseDir}.
761      * Returns a {@code File} object representing the directory on success, {@code null} on
762      * failure.
763      */
createDir(File baseDir, String name)764     public static @Nullable File createDir(File baseDir, String name) {
765         final File dir = new File(baseDir, name);
766 
767         if (dir.exists()) {
768             return dir.isDirectory() ? dir : null;
769         }
770 
771         return dir.mkdir() ? dir : null;
772     }
773 
774     /**
775      * Round the given size of a storage device to a nice round power-of-two
776      * value, such as 256MB or 32GB. This avoids showing weird values like
777      * "29.5GB" in UI.
778      */
roundStorageSize(long size)779     public static long roundStorageSize(long size) {
780         long val = 1;
781         long pow = 1;
782         while ((val * pow) < size) {
783             val <<= 1;
784             if (val > 512) {
785                 val = 1;
786                 pow *= 1000;
787             }
788         }
789         return val * pow;
790     }
791 }
792