Home | History | Annotate | Download | only in os
      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      */
     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      */
    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      */
    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 
    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      */
    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      */
    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
    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
    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
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    448     public static boolean deleteContentsAndDir(File dir) {
    449         if (deleteContents(dir)) {
    450             return dir.delete();
    451         } else {
    452             return false;
    453         }
    454     }
    455 
    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 
    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      */
    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      */
    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 
    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      */
    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      */
    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
    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 
    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 
    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 
    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      */
    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 
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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      */
    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      */
    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