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