1 /* 2 * Copyright (C) 2019 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.providers.media.util; 18 19 import static android.os.ParcelFileDescriptor.MODE_APPEND; 20 import static android.os.ParcelFileDescriptor.MODE_CREATE; 21 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 22 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE; 23 import static android.os.ParcelFileDescriptor.MODE_TRUNCATE; 24 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY; 25 import static android.system.OsConstants.F_OK; 26 import static android.system.OsConstants.O_ACCMODE; 27 import static android.system.OsConstants.O_APPEND; 28 import static android.system.OsConstants.O_CLOEXEC; 29 import static android.system.OsConstants.O_CREAT; 30 import static android.system.OsConstants.O_NOFOLLOW; 31 import static android.system.OsConstants.O_RDONLY; 32 import static android.system.OsConstants.O_RDWR; 33 import static android.system.OsConstants.O_TRUNC; 34 import static android.system.OsConstants.O_WRONLY; 35 import static android.system.OsConstants.R_OK; 36 import static android.system.OsConstants.S_IRWXG; 37 import static android.system.OsConstants.S_IRWXU; 38 import static android.system.OsConstants.W_OK; 39 40 import static com.android.providers.media.util.DatabaseUtils.getAsBoolean; 41 import static com.android.providers.media.util.DatabaseUtils.getAsLong; 42 import static com.android.providers.media.util.DatabaseUtils.parseBoolean; 43 import static com.android.providers.media.util.Logging.TAG; 44 45 import android.content.ClipDescription; 46 import android.content.ContentValues; 47 import android.content.Context; 48 import android.net.Uri; 49 import android.os.Environment; 50 import android.os.ParcelFileDescriptor; 51 import android.os.storage.StorageManager; 52 import android.provider.MediaStore; 53 import android.provider.MediaStore.MediaColumns; 54 import android.system.ErrnoException; 55 import android.system.Os; 56 import android.system.OsConstants; 57 import android.text.TextUtils; 58 import android.text.format.DateUtils; 59 import android.util.Log; 60 import android.webkit.MimeTypeMap; 61 62 import androidx.annotation.NonNull; 63 import androidx.annotation.Nullable; 64 import androidx.annotation.VisibleForTesting; 65 66 import java.io.File; 67 import java.io.FileDescriptor; 68 import java.io.FileNotFoundException; 69 import java.io.IOException; 70 import java.io.InputStream; 71 import java.io.OutputStream; 72 import java.nio.charset.StandardCharsets; 73 import java.nio.file.FileVisitResult; 74 import java.nio.file.FileVisitor; 75 import java.nio.file.Files; 76 import java.nio.file.NoSuchFileException; 77 import java.nio.file.Path; 78 import java.nio.file.attribute.BasicFileAttributes; 79 import java.util.ArrayList; 80 import java.util.Arrays; 81 import java.util.Collection; 82 import java.util.Comparator; 83 import java.util.Iterator; 84 import java.util.Locale; 85 import java.util.Objects; 86 import java.util.Optional; 87 import java.util.function.Consumer; 88 import java.util.regex.Matcher; 89 import java.util.regex.Pattern; 90 91 public class FileUtils { 92 /** 93 * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)} 94 * which adds security features like {@link OsConstants#O_CLOEXEC} and 95 * {@link OsConstants#O_NOFOLLOW}. 96 */ openSafely(@onNull File file, int pfdFlags)97 public static @NonNull ParcelFileDescriptor openSafely(@NonNull File file, int pfdFlags) 98 throws FileNotFoundException { 99 final int posixFlags = translateModePfdToPosix(pfdFlags) | O_CLOEXEC | O_NOFOLLOW; 100 try { 101 final FileDescriptor fd = Os.open(file.getAbsolutePath(), posixFlags, 102 S_IRWXU | S_IRWXG); 103 try { 104 return ParcelFileDescriptor.dup(fd); 105 } finally { 106 closeQuietly(fd); 107 } 108 } catch (IOException | ErrnoException e) { 109 throw new FileNotFoundException(e.getMessage()); 110 } 111 } 112 closeQuietly(@ullable AutoCloseable closeable)113 public static void closeQuietly(@Nullable AutoCloseable closeable) { 114 android.os.FileUtils.closeQuietly(closeable); 115 } 116 closeQuietly(@ullable FileDescriptor fd)117 public static void closeQuietly(@Nullable FileDescriptor fd) { 118 if (fd == null) return; 119 try { 120 Os.close(fd); 121 } catch (ErrnoException ignored) { 122 } 123 } 124 copy(@onNull InputStream in, @NonNull OutputStream out)125 public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { 126 return android.os.FileUtils.copy(in, out); 127 } 128 buildPath(File base, String... segments)129 public static File buildPath(File base, String... segments) { 130 File cur = base; 131 for (String segment : segments) { 132 if (cur == null) { 133 cur = new File(segment); 134 } else { 135 cur = new File(cur, segment); 136 } 137 } 138 return cur; 139 } 140 141 /** 142 * Delete older files in a directory until only those matching the given 143 * constraints remain. 144 * 145 * @param minCount Always keep at least this many files. 146 * @param minAgeMs Always keep files younger than this age, in milliseconds. 147 * @return if any files were deleted. 148 */ deleteOlderFiles(File dir, int minCount, long minAgeMs)149 public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) { 150 if (minCount < 0 || minAgeMs < 0) { 151 throw new IllegalArgumentException("Constraints must be positive or 0"); 152 } 153 154 final File[] files = dir.listFiles(); 155 if (files == null) return false; 156 157 // Sort with newest files first 158 Arrays.sort(files, new Comparator<File>() { 159 @Override 160 public int compare(File lhs, File rhs) { 161 return Long.compare(rhs.lastModified(), lhs.lastModified()); 162 } 163 }); 164 165 // Keep at least minCount files 166 boolean deleted = false; 167 for (int i = minCount; i < files.length; i++) { 168 final File file = files[i]; 169 170 // Keep files newer than minAgeMs 171 final long age = System.currentTimeMillis() - file.lastModified(); 172 if (age > minAgeMs) { 173 if (file.delete()) { 174 Log.d(TAG, "Deleted old file " + file); 175 deleted = true; 176 } 177 } 178 } 179 return deleted; 180 } 181 182 /** 183 * Shamelessly borrowed from {@code android.os.FileUtils}. 184 */ translateModeStringToPosix(String mode)185 public static int translateModeStringToPosix(String mode) { 186 // Sanity check for invalid chars 187 for (int i = 0; i < mode.length(); i++) { 188 switch (mode.charAt(i)) { 189 case 'r': 190 case 'w': 191 case 't': 192 case 'a': 193 break; 194 default: 195 throw new IllegalArgumentException("Bad mode: " + mode); 196 } 197 } 198 199 int res = 0; 200 if (mode.startsWith("rw")) { 201 res = O_RDWR | O_CREAT; 202 } else if (mode.startsWith("w")) { 203 res = O_WRONLY | O_CREAT; 204 } else if (mode.startsWith("r")) { 205 res = O_RDONLY; 206 } else { 207 throw new IllegalArgumentException("Bad mode: " + mode); 208 } 209 if (mode.indexOf('t') != -1) { 210 res |= O_TRUNC; 211 } 212 if (mode.indexOf('a') != -1) { 213 res |= O_APPEND; 214 } 215 return res; 216 } 217 218 /** 219 * Shamelessly borrowed from {@code android.os.FileUtils}. 220 */ translateModePosixToString(int mode)221 public static String translateModePosixToString(int mode) { 222 String res = ""; 223 if ((mode & O_ACCMODE) == O_RDWR) { 224 res = "rw"; 225 } else if ((mode & O_ACCMODE) == O_WRONLY) { 226 res = "w"; 227 } else if ((mode & O_ACCMODE) == O_RDONLY) { 228 res = "r"; 229 } else { 230 throw new IllegalArgumentException("Bad mode: " + mode); 231 } 232 if ((mode & O_TRUNC) == O_TRUNC) { 233 res += "t"; 234 } 235 if ((mode & O_APPEND) == O_APPEND) { 236 res += "a"; 237 } 238 return res; 239 } 240 241 /** 242 * Shamelessly borrowed from {@code android.os.FileUtils}. 243 */ translateModePosixToPfd(int mode)244 public static int translateModePosixToPfd(int mode) { 245 int res = 0; 246 if ((mode & O_ACCMODE) == O_RDWR) { 247 res = MODE_READ_WRITE; 248 } else if ((mode & O_ACCMODE) == O_WRONLY) { 249 res = MODE_WRITE_ONLY; 250 } else if ((mode & O_ACCMODE) == O_RDONLY) { 251 res = MODE_READ_ONLY; 252 } else { 253 throw new IllegalArgumentException("Bad mode: " + mode); 254 } 255 if ((mode & O_CREAT) == O_CREAT) { 256 res |= MODE_CREATE; 257 } 258 if ((mode & O_TRUNC) == O_TRUNC) { 259 res |= MODE_TRUNCATE; 260 } 261 if ((mode & O_APPEND) == O_APPEND) { 262 res |= MODE_APPEND; 263 } 264 return res; 265 } 266 267 /** 268 * Shamelessly borrowed from {@code android.os.FileUtils}. 269 */ translateModePfdToPosix(int mode)270 public static int translateModePfdToPosix(int mode) { 271 int res = 0; 272 if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) { 273 res = O_RDWR; 274 } else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) { 275 res = O_WRONLY; 276 } else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) { 277 res = O_RDONLY; 278 } else { 279 throw new IllegalArgumentException("Bad mode: " + mode); 280 } 281 if ((mode & MODE_CREATE) == MODE_CREATE) { 282 res |= O_CREAT; 283 } 284 if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) { 285 res |= O_TRUNC; 286 } 287 if ((mode & MODE_APPEND) == MODE_APPEND) { 288 res |= O_APPEND; 289 } 290 return res; 291 } 292 293 /** 294 * Shamelessly borrowed from {@code android.os.FileUtils}. 295 */ translateModeAccessToPosix(int mode)296 public static int translateModeAccessToPosix(int mode) { 297 if (mode == F_OK) { 298 // There's not an exact mapping, so we attempt a read-only open to 299 // determine if a file exists 300 return O_RDONLY; 301 } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) { 302 return O_RDWR; 303 } else if ((mode & R_OK) == R_OK) { 304 return O_RDONLY; 305 } else if ((mode & W_OK) == W_OK) { 306 return O_WRONLY; 307 } else { 308 throw new IllegalArgumentException("Bad mode: " + mode); 309 } 310 } 311 312 /** 313 * Test if a file lives under the given directory, either as a direct child 314 * or a distant grandchild. 315 * <p> 316 * Both files <em>must</em> have been resolved using 317 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 318 * attacks. 319 * 320 * @hide 321 */ contains(File[] dirs, File file)322 public static boolean contains(File[] dirs, File file) { 323 for (File dir : dirs) { 324 if (contains(dir, file)) { 325 return true; 326 } 327 } 328 return false; 329 } 330 331 /** {@hide} */ contains(Collection<File> dirs, File file)332 public static boolean contains(Collection<File> dirs, File file) { 333 for (File dir : dirs) { 334 if (contains(dir, file)) { 335 return true; 336 } 337 } 338 return false; 339 } 340 341 /** 342 * Test if a file lives under the given directory, either as a direct child 343 * or a distant grandchild. 344 * <p> 345 * Both files <em>must</em> have been resolved using 346 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 347 * attacks. 348 * 349 * @hide 350 */ contains(File dir, File file)351 public static boolean contains(File dir, File file) { 352 if (dir == null || file == null) return false; 353 return contains(dir.getAbsolutePath(), file.getAbsolutePath()); 354 } 355 356 /** 357 * Test if a file lives under the given directory, either as a direct child 358 * or a distant grandchild. 359 * <p> 360 * Both files <em>must</em> have been resolved using 361 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 362 * attacks. 363 * 364 * @hide 365 */ contains(String dirPath, String filePath)366 public static boolean contains(String dirPath, String filePath) { 367 if (dirPath.equals(filePath)) { 368 return true; 369 } 370 if (!dirPath.endsWith("/")) { 371 dirPath += "/"; 372 } 373 return filePath.startsWith(dirPath); 374 } 375 376 /** 377 * Write {@link String} to the given {@link File}. Deletes any existing file 378 * when the argument is {@link Optional#empty()}. 379 */ writeString(@onNull File file, @NonNull Optional<String> value)380 public static void writeString(@NonNull File file, @NonNull Optional<String> value) 381 throws IOException { 382 if (value.isPresent()) { 383 Files.write(file.toPath(), value.get().getBytes(StandardCharsets.UTF_8)); 384 } else { 385 file.delete(); 386 } 387 } 388 389 /** 390 * Read given {@link File} as a single {@link String}. Returns 391 * {@link Optional#empty()} when the file doesn't exist. 392 */ readString(@onNull File file)393 public static @NonNull Optional<String> readString(@NonNull File file) throws IOException { 394 try { 395 final String value = new String(Files.readAllBytes(file.toPath()), 396 StandardCharsets.UTF_8); 397 return Optional.of(value); 398 } catch (NoSuchFileException e) { 399 return Optional.empty(); 400 } 401 } 402 403 /** 404 * Recursively walk the contents of the given {@link Path}, invoking the 405 * given {@link Consumer} for every file and directory encountered. This is 406 * typically used for recursively deleting a directory tree. 407 * <p> 408 * Gracefully attempts to process as much as possible in the face of any 409 * failures. 410 */ walkFileTreeContents(@onNull Path path, @NonNull Consumer<Path> operation)411 public static void walkFileTreeContents(@NonNull Path path, @NonNull Consumer<Path> operation) { 412 try { 413 Files.walkFileTree(path, new FileVisitor<Path>() { 414 @Override 415 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { 416 return FileVisitResult.CONTINUE; 417 } 418 419 @Override 420 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 421 if (!Objects.equals(path, file)) { 422 operation.accept(file); 423 } 424 return FileVisitResult.CONTINUE; 425 } 426 427 @Override 428 public FileVisitResult visitFileFailed(Path file, IOException e) { 429 Log.w(TAG, "Failed to visit " + file, e); 430 return FileVisitResult.CONTINUE; 431 } 432 433 @Override 434 public FileVisitResult postVisitDirectory(Path dir, IOException e) { 435 if (!Objects.equals(path, dir)) { 436 operation.accept(dir); 437 } 438 return FileVisitResult.CONTINUE; 439 } 440 }); 441 } catch (IOException e) { 442 Log.w(TAG, "Failed to walk " + path, e); 443 } 444 } 445 446 /** 447 * Recursively delete all contents inside the given directory. Gracefully 448 * attempts to delete as much as possible in the face of any failures. 449 * 450 * @deprecated if you're calling this from inside {@code MediaProvider}, you 451 * likely want to call {@link #forEach} with a separate 452 * invocation to invalidate FUSE entries. 453 */ 454 @Deprecated deleteContents(@onNull File dir)455 public static void deleteContents(@NonNull File dir) { 456 walkFileTreeContents(dir.toPath(), (path) -> { 457 path.toFile().delete(); 458 }); 459 } 460 isValidFatFilenameChar(char c)461 private static boolean isValidFatFilenameChar(char c) { 462 if ((0x00 <= c && c <= 0x1f)) { 463 return false; 464 } 465 switch (c) { 466 case '"': 467 case '*': 468 case '/': 469 case ':': 470 case '<': 471 case '>': 472 case '?': 473 case '\\': 474 case '|': 475 case 0x7F: 476 return false; 477 default: 478 return true; 479 } 480 } 481 482 /** 483 * Check if given filename is valid for a FAT filesystem. 484 * 485 * @hide 486 */ isValidFatFilename(String name)487 public static boolean isValidFatFilename(String name) { 488 return (name != null) && name.equals(buildValidFatFilename(name)); 489 } 490 491 /** 492 * Mutate the given filename to make it valid for a FAT filesystem, 493 * replacing any invalid characters with "_". 494 * 495 * @hide 496 */ buildValidFatFilename(String name)497 public static String buildValidFatFilename(String name) { 498 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 499 return "(invalid)"; 500 } 501 final StringBuilder res = new StringBuilder(name.length()); 502 for (int i = 0; i < name.length(); i++) { 503 final char c = name.charAt(i); 504 if (isValidFatFilenameChar(c)) { 505 res.append(c); 506 } else { 507 res.append('_'); 508 } 509 } 510 // Even though vfat allows 255 UCS-2 chars, we might eventually write to 511 // ext4 through a FUSE layer, so use that limit. 512 trimFilename(res, 255); 513 return res.toString(); 514 } 515 516 /** {@hide} */ 517 // @VisibleForTesting trimFilename(String str, int maxBytes)518 public static String trimFilename(String str, int maxBytes) { 519 final StringBuilder res = new StringBuilder(str); 520 trimFilename(res, maxBytes); 521 return res.toString(); 522 } 523 524 /** {@hide} */ trimFilename(StringBuilder res, int maxBytes)525 private static void trimFilename(StringBuilder res, int maxBytes) { 526 byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8); 527 if (raw.length > maxBytes) { 528 maxBytes -= 3; 529 while (raw.length > maxBytes) { 530 res.deleteCharAt(res.length() / 2); 531 raw = res.toString().getBytes(StandardCharsets.UTF_8); 532 } 533 res.insert(res.length() / 2, "..."); 534 } 535 } 536 537 /** {@hide} */ buildUniqueFileWithExtension(File parent, String name, String ext)538 private static File buildUniqueFileWithExtension(File parent, String name, String ext) 539 throws FileNotFoundException { 540 final Iterator<String> names = buildUniqueNameIterator(parent, name); 541 while (names.hasNext()) { 542 File file = buildFile(parent, names.next(), ext); 543 if (!file.exists()) { 544 return file; 545 } 546 } 547 throw new FileNotFoundException("Failed to create unique file"); 548 } 549 550 private static final Pattern PATTERN_DCF_STRICT = Pattern 551 .compile("([A-Z0-9_]{4})([0-9]{4})"); 552 private static final Pattern PATTERN_DCF_RELAXED = Pattern 553 .compile("((?:IMG|MVIMG|VID)_[0-9]{8}_[0-9]{6})(?:~([0-9]+))?"); 554 isDcim(@onNull File dir)555 private static boolean isDcim(@NonNull File dir) { 556 while (dir != null) { 557 if (Objects.equals("DCIM", dir.getName())) { 558 return true; 559 } 560 dir = dir.getParentFile(); 561 } 562 return false; 563 } 564 buildUniqueNameIterator(@onNull File parent, @NonNull String name)565 private static @NonNull Iterator<String> buildUniqueNameIterator(@NonNull File parent, 566 @NonNull String name) { 567 if (isDcim(parent)) { 568 final Matcher dcfStrict = PATTERN_DCF_STRICT.matcher(name); 569 if (dcfStrict.matches()) { 570 // Generate names like "IMG_1001" 571 final String prefix = dcfStrict.group(1); 572 return new Iterator<String>() { 573 int i = Integer.parseInt(dcfStrict.group(2)); 574 @Override 575 public String next() { 576 final String res = String.format("%s%04d", prefix, i); 577 i++; 578 return res; 579 } 580 @Override 581 public boolean hasNext() { 582 return i <= 9999; 583 } 584 }; 585 } 586 587 final Matcher dcfRelaxed = PATTERN_DCF_RELAXED.matcher(name); 588 if (dcfRelaxed.matches()) { 589 // Generate names like "IMG_20190102_030405~2" 590 final String prefix = dcfRelaxed.group(1); 591 return new Iterator<String>() { 592 int i = TextUtils.isEmpty(dcfRelaxed.group(2)) ? 1 593 : Integer.parseInt(dcfRelaxed.group(2)); 594 @Override 595 public String next() { 596 final String res = (i == 1) ? prefix : String.format("%s~%d", prefix, i); 597 i++; 598 return res; 599 } 600 @Override 601 public boolean hasNext() { 602 return i <= 99; 603 } 604 }; 605 } 606 } 607 608 // Generate names like "foo (2)" 609 return new Iterator<String>() { 610 int i = 0; 611 @Override 612 public String next() { 613 final String res = (i == 0) ? name : name + " (" + i + ")"; 614 i++; 615 return res; 616 } 617 @Override 618 public boolean hasNext() { 619 return i < 32; 620 } 621 }; 622 } 623 624 /** 625 * Generates a unique file name under the given parent directory. If the display name doesn't 626 * have an extension that matches the requested MIME type, the default extension for that MIME 627 * type is appended. If a file already exists, the name is appended with a numerical value to 628 * make it unique. 629 * 630 * For example, the display name 'example' with 'text/plain' MIME might produce 631 * 'example.txt' or 'example (1).txt', etc. 632 * 633 * @throws FileNotFoundException 634 * @hide 635 */ 636 public static File buildUniqueFile(File parent, String mimeType, String displayName) 637 throws FileNotFoundException { 638 final String[] parts = splitFileName(mimeType, displayName); 639 return buildUniqueFileWithExtension(parent, parts[0], parts[1]); 640 } 641 642 /** {@hide} */ 643 public static File buildNonUniqueFile(File parent, String mimeType, String displayName) { 644 final String[] parts = splitFileName(mimeType, displayName); 645 return buildFile(parent, parts[0], parts[1]); 646 } 647 648 /** 649 * Generates a unique file name under the given parent directory, keeping 650 * any extension intact. 651 * 652 * @hide 653 */ 654 public static File buildUniqueFile(File parent, String displayName) 655 throws FileNotFoundException { 656 final String name; 657 final String ext; 658 659 // Extract requested extension from display name 660 final int lastDot = displayName.lastIndexOf('.'); 661 if (lastDot >= 0) { 662 name = displayName.substring(0, lastDot); 663 ext = displayName.substring(lastDot + 1); 664 } else { 665 name = displayName; 666 ext = null; 667 } 668 669 return buildUniqueFileWithExtension(parent, name, ext); 670 } 671 672 /** 673 * Splits file name into base name and extension. 674 * If the display name doesn't have an extension that matches the requested MIME type, the 675 * extension is regarded as a part of filename and default extension for that MIME type is 676 * appended. 677 * 678 * @hide 679 */ 680 public static String[] splitFileName(String mimeType, String displayName) { 681 String name; 682 String ext; 683 684 { 685 String mimeTypeFromExt; 686 687 // Extract requested extension from display name 688 final int lastDot = displayName.lastIndexOf('.'); 689 if (lastDot > 0) { 690 name = displayName.substring(0, lastDot); 691 ext = displayName.substring(lastDot + 1); 692 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 693 ext.toLowerCase(Locale.ROOT)); 694 } else { 695 name = displayName; 696 ext = null; 697 mimeTypeFromExt = null; 698 } 699 700 if (mimeTypeFromExt == null) { 701 mimeTypeFromExt = ClipDescription.MIMETYPE_UNKNOWN; 702 } 703 704 final String extFromMimeType; 705 if (ClipDescription.MIMETYPE_UNKNOWN.equalsIgnoreCase(mimeType)) { 706 extFromMimeType = null; 707 } else { 708 extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 709 } 710 711 if (MimeUtils.equalIgnoreCase(mimeType, mimeTypeFromExt) 712 || MimeUtils.equalIgnoreCase(ext, extFromMimeType)) { 713 // Extension maps back to requested MIME type; allow it 714 } else { 715 // No match; insist that create file matches requested MIME 716 name = displayName; 717 ext = extFromMimeType; 718 } 719 } 720 721 if (ext == null) { 722 ext = ""; 723 } 724 725 return new String[] { name, ext }; 726 } 727 728 /** {@hide} */ 729 private static File buildFile(File parent, String name, String ext) { 730 if (TextUtils.isEmpty(ext)) { 731 return new File(parent, name); 732 } else { 733 return new File(parent, name + "." + ext); 734 } 735 } 736 737 public static @Nullable String extractDisplayName(@Nullable String data) { 738 if (data == null) return null; 739 if (data.indexOf('/') == -1) { 740 return data; 741 } 742 if (data.endsWith("/")) { 743 data = data.substring(0, data.length() - 1); 744 } 745 return data.substring(data.lastIndexOf('/') + 1); 746 } 747 748 public static @Nullable String extractFileName(@Nullable String data) { 749 if (data == null) return null; 750 data = extractDisplayName(data); 751 752 final int lastDot = data.lastIndexOf('.'); 753 if (lastDot == -1) { 754 return data; 755 } else { 756 return data.substring(0, lastDot); 757 } 758 } 759 760 public static @Nullable String extractFileExtension(@Nullable String data) { 761 if (data == null) return null; 762 data = extractDisplayName(data); 763 764 final int lastDot = data.lastIndexOf('.'); 765 if (lastDot == -1) { 766 return null; 767 } else { 768 return data.substring(lastDot + 1); 769 } 770 } 771 772 /** 773 * Return list of paths that should be scanned with 774 * {@link com.android.providers.media.scan.MediaScanner} for the given 775 * volume name. 776 */ 777 public static @NonNull Collection<File> getVolumeScanPaths(@NonNull Context context, 778 @NonNull String volumeName) throws FileNotFoundException { 779 final ArrayList<File> res = new ArrayList<>(); 780 switch (volumeName) { 781 case MediaStore.VOLUME_INTERNAL: { 782 res.addAll(Environment.getInternalMediaDirectories()); 783 break; 784 } 785 case MediaStore.VOLUME_EXTERNAL: { 786 for (String resolvedVolumeName : MediaStore.getExternalVolumeNames(context)) { 787 res.add(getVolumePath(context, resolvedVolumeName)); 788 } 789 break; 790 } 791 default: { 792 res.add(getVolumePath(context, volumeName)); 793 } 794 } 795 return res; 796 } 797 798 /** 799 * Return path where the given volume name is mounted. 800 */ 801 public static @NonNull File getVolumePath(@NonNull Context context, 802 @NonNull String volumeName) throws FileNotFoundException { 803 switch (volumeName) { 804 case MediaStore.VOLUME_INTERNAL: 805 case MediaStore.VOLUME_EXTERNAL: 806 throw new FileNotFoundException(volumeName + " has no associated path"); 807 } 808 809 final Uri uri = MediaStore.Files.getContentUri(volumeName); 810 final File path = context.getSystemService(StorageManager.class).getStorageVolume(uri) 811 .getDirectory(); 812 if (path != null) { 813 return path; 814 } else { 815 throw new FileNotFoundException(volumeName + " has no associated path"); 816 } 817 } 818 819 /** 820 * Returns the content URI for the volume that contains the given path. 821 * 822 * <p>{@link MediaStore.Files#getContentUriForPath(String)} can't detect public volumes and can 823 * only return the URI for the primary external storage, that's why this utility should be used 824 * instead. 825 */ 826 public static @NonNull Uri getContentUriForPath(@NonNull String path) { 827 Objects.requireNonNull(path); 828 return MediaStore.Files.getContentUri(extractVolumeName(path)); 829 } 830 831 /** 832 * Return volume name which hosts the given path. 833 */ 834 public static @NonNull String getVolumeName(@NonNull Context context, @NonNull File path) { 835 if (contains(Environment.getStorageDirectory(), path)) { 836 return context.getSystemService(StorageManager.class).getStorageVolume(path) 837 .getMediaStoreVolumeName(); 838 } else { 839 return MediaStore.VOLUME_INTERNAL; 840 } 841 } 842 843 public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile( 844 "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/.+"); 845 public static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile( 846 "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/?"); 847 public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile( 848 "(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$"); 849 public static final Pattern PATTERN_PENDING_FILEPATH_FOR_SQL = Pattern.compile( 850 ".*/\\.pending-(\\d+)-([^/]+)$"); 851 852 /** 853 * File prefix indicating that the file {@link MediaColumns#IS_PENDING}. 854 */ 855 public static final String PREFIX_PENDING = "pending"; 856 857 /** 858 * File prefix indicating that the file {@link MediaColumns#IS_TRASHED}. 859 */ 860 public static final String PREFIX_TRASHED = "trashed"; 861 862 /** 863 * Default duration that {@link MediaColumns#IS_PENDING} items should be 864 * preserved for until automatically cleaned by {@link #runIdleMaintenance}. 865 */ 866 public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS; 867 868 /** 869 * Default duration that {@link MediaColumns#IS_TRASHED} items should be 870 * preserved for until automatically cleaned by {@link #runIdleMaintenance}. 871 */ 872 public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS; 873 874 public static boolean isDownload(@NonNull String path) { 875 return PATTERN_DOWNLOADS_FILE.matcher(path).matches(); 876 } 877 878 public static boolean isDownloadDir(@NonNull String path) { 879 return PATTERN_DOWNLOADS_DIRECTORY.matcher(path).matches(); 880 } 881 882 /** 883 * Regex that matches paths in all well-known package-specific directories, 884 * and which captures the package name as the first group. 885 */ 886 public static final Pattern PATTERN_OWNED_PATH = Pattern.compile( 887 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/?.*)?"); 888 889 /** 890 * Regex that matches Android/obb or Android/data path. 891 */ 892 public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile( 893 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/?$"); 894 895 /** 896 * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it 897 * captures both top-level paths and sandboxed paths. 898 */ 899 private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile( 900 "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)(Android/sandbox/([^/]+)/)?"); 901 902 /** 903 * Regex that matches paths under well-known storage paths. 904 */ 905 private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile( 906 "(?i)^/storage/([^/]+)"); 907 908 private static @Nullable String normalizeUuid(@Nullable String fsUuid) { 909 return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null; 910 } 911 912 public static @Nullable String extractVolumePath(@Nullable String data) { 913 if (data == null) return null; 914 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 915 if (matcher.find()) { 916 return data.substring(0, matcher.end()); 917 } else { 918 return null; 919 } 920 } 921 922 public static @Nullable String extractVolumeName(@Nullable String data) { 923 if (data == null) return null; 924 final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data); 925 if (matcher.find()) { 926 final String volumeName = matcher.group(1); 927 if (volumeName.equals("emulated")) { 928 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 929 } else { 930 return normalizeUuid(volumeName); 931 } 932 } else { 933 return MediaStore.VOLUME_INTERNAL; 934 } 935 } 936 937 public static @Nullable String extractRelativePath(@Nullable String data) { 938 if (data == null) return null; 939 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 940 if (matcher.find()) { 941 final int lastSlash = data.lastIndexOf('/'); 942 if (lastSlash == -1 || lastSlash < matcher.end()) { 943 // This is a file in the top-level directory, so relative path is "/" 944 // which is different than null, which means unknown path 945 return "/"; 946 } else { 947 return data.substring(matcher.end(), lastSlash + 1); 948 } 949 } else { 950 return null; 951 } 952 } 953 954 /** 955 * Returns relative path for the directory. 956 */ 957 @VisibleForTesting 958 public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) { 959 if (directoryPath == null) return null; 960 961 if (directoryPath.equals("/storage/emulated") || 962 directoryPath.equals("/storage/emulated/")) { 963 // This path is not reachable for MediaProvider. 964 return null; 965 } 966 967 // We are extracting relative path for the directory itself, we add "/" so that we can use 968 // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative 969 // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for 970 // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/". 971 if (!directoryPath.endsWith("/")) { 972 // Relative path for directory should end with "/". 973 directoryPath += "/"; 974 } 975 976 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath); 977 if (matcher.find()) { 978 if (matcher.end() == directoryPath.length()) { 979 // This is the top-level directory, so relative path is "/" 980 return "/"; 981 } 982 return directoryPath.substring(matcher.end()); 983 } 984 return null; 985 } 986 987 public static @Nullable String extractPathOwnerPackageName(@Nullable String path) { 988 if (path == null) return null; 989 final Matcher m = PATTERN_OWNED_PATH.matcher(path); 990 if (m.matches()) { 991 return m.group(1); 992 } else { 993 return null; 994 } 995 } 996 997 /** 998 * Returns true if relative path is Android/data or Android/obb path. 999 */ 1000 public static boolean isDataOrObbPath(String path) { 1001 if (path == null) return false; 1002 final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path); 1003 return m.matches(); 1004 } 1005 1006 /** 1007 * Returns the name of the top level directory, or null if the path doesn't go through the 1008 * external storage directory. 1009 */ 1010 @Nullable 1011 public static String extractTopLevelDir(String path) { 1012 final String relativePath = extractRelativePath(path); 1013 if (relativePath == null) { 1014 return null; 1015 } 1016 final String[] relativePathSegments = relativePath.split("/"); 1017 return relativePathSegments.length > 0 ? relativePathSegments[0] : null; 1018 } 1019 1020 /** 1021 * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other 1022 * columns being modified by this operation. 1023 */ 1024 public static void computeDateExpires(@NonNull ContentValues values) { 1025 // External apps have no ability to change this field 1026 values.remove(MediaColumns.DATE_EXPIRES); 1027 1028 // Only define the field when this modification is actually adjusting 1029 // one of the flags that should influence the expiration 1030 final Object pending = values.get(MediaColumns.IS_PENDING); 1031 if (pending != null) { 1032 if (parseBoolean(pending, false)) { 1033 values.put(MediaColumns.DATE_EXPIRES, 1034 (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000); 1035 } else { 1036 values.putNull(MediaColumns.DATE_EXPIRES); 1037 } 1038 } 1039 final Object trashed = values.get(MediaColumns.IS_TRASHED); 1040 if (trashed != null) { 1041 if (parseBoolean(trashed, false)) { 1042 values.put(MediaColumns.DATE_EXPIRES, 1043 (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000); 1044 } else { 1045 values.putNull(MediaColumns.DATE_EXPIRES); 1046 } 1047 } 1048 } 1049 1050 /** 1051 * Compute several scattered {@link MediaColumns} values from 1052 * {@link MediaColumns#DATA}. This method performs no enforcement of 1053 * argument validity. 1054 */ 1055 public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) { 1056 // Worst case we have to assume no bucket details 1057 values.remove(MediaColumns.VOLUME_NAME); 1058 values.remove(MediaColumns.RELATIVE_PATH); 1059 values.remove(MediaColumns.IS_TRASHED); 1060 values.remove(MediaColumns.DATE_EXPIRES); 1061 values.remove(MediaColumns.DISPLAY_NAME); 1062 values.remove(MediaColumns.BUCKET_ID); 1063 values.remove(MediaColumns.BUCKET_DISPLAY_NAME); 1064 1065 final String data = values.getAsString(MediaColumns.DATA); 1066 if (TextUtils.isEmpty(data)) return; 1067 1068 final File file = new File(data); 1069 final File fileLower = new File(data.toLowerCase(Locale.ROOT)); 1070 1071 values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data)); 1072 values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data)); 1073 final String displayName = extractDisplayName(data); 1074 final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName); 1075 if (matcher.matches()) { 1076 values.put(MediaColumns.IS_PENDING, 1077 matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0); 1078 values.put(MediaColumns.IS_TRASHED, 1079 matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0); 1080 values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2))); 1081 values.put(MediaColumns.DISPLAY_NAME, matcher.group(3)); 1082 } else { 1083 if (isForFuse) { 1084 // Allow Fuse thread to set IS_PENDING when using DATA column. 1085 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify 1086 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't 1087 // explicitly specify the value of IS_PENDING. 1088 } else { 1089 values.put(MediaColumns.IS_PENDING, 0); 1090 } 1091 values.put(MediaColumns.IS_TRASHED, 0); 1092 values.putNull(MediaColumns.DATE_EXPIRES); 1093 values.put(MediaColumns.DISPLAY_NAME, displayName); 1094 } 1095 1096 // Buckets are the parent directory 1097 final String parent = fileLower.getParent(); 1098 if (parent != null) { 1099 values.put(MediaColumns.BUCKET_ID, parent.hashCode()); 1100 // The relative path for files in the top directory is "/" 1101 if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) { 1102 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName()); 1103 } 1104 } 1105 } 1106 1107 /** 1108 * Compute {@link MediaColumns#DATA} from several scattered 1109 * {@link MediaColumns} values. This method performs no enforcement of 1110 * argument validity. 1111 */ 1112 public static void computeDataFromValues(@NonNull ContentValues values, 1113 @NonNull File volumePath, boolean isForFuse) { 1114 values.remove(MediaColumns.DATA); 1115 1116 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 1117 final String resolvedDisplayName; 1118 // Pending file path shouldn't be rewritten for files inserted via filepath. 1119 if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) { 1120 final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES, 1121 (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000); 1122 resolvedDisplayName = String.format(".%s-%d-%s", 1123 FileUtils.PREFIX_PENDING, dateExpires, displayName); 1124 } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) { 1125 final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES, 1126 (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000); 1127 resolvedDisplayName = String.format(".%s-%d-%s", 1128 FileUtils.PREFIX_TRASHED, dateExpires, displayName); 1129 } else { 1130 resolvedDisplayName = displayName; 1131 } 1132 1133 final File filePath = buildPath(volumePath, 1134 values.getAsString(MediaColumns.RELATIVE_PATH), resolvedDisplayName); 1135 values.put(MediaColumns.DATA, filePath.getAbsolutePath()); 1136 } 1137 1138 public static void sanitizeValues(@NonNull ContentValues values, 1139 boolean rewriteHiddenFileName) { 1140 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); 1141 for (int i = 0; i < relativePath.length; i++) { 1142 relativePath[i] = sanitizeDisplayName(relativePath[i], rewriteHiddenFileName); 1143 } 1144 values.put(MediaColumns.RELATIVE_PATH, 1145 String.join("/", relativePath) + "/"); 1146 1147 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 1148 values.put(MediaColumns.DISPLAY_NAME, 1149 sanitizeDisplayName(displayName, rewriteHiddenFileName)); 1150 } 1151 1152 /** {@hide} **/ 1153 @Nullable 1154 public static String getAbsoluteSanitizedPath(String path) { 1155 final String[] pathSegments = sanitizePath(path); 1156 if (pathSegments.length == 0) { 1157 return null; 1158 } 1159 return path = "/" + String.join("/", 1160 Arrays.copyOfRange(pathSegments, 1, pathSegments.length)); 1161 } 1162 1163 /** {@hide} */ 1164 public static @NonNull String[] sanitizePath(@Nullable String path) { 1165 if (path == null) { 1166 return new String[0]; 1167 } else { 1168 final String[] segments = path.split("/"); 1169 // If the path corresponds to the top level directory, then we return an empty path 1170 // which denotes the top level directory 1171 if (segments.length == 0) { 1172 return new String[] { "" }; 1173 } 1174 for (int i = 0; i < segments.length; i++) { 1175 segments[i] = sanitizeDisplayName(segments[i]); 1176 } 1177 return segments; 1178 } 1179 } 1180 1181 /** 1182 * Sanitizes given name by mutating the file name to make it valid for a FAT filesystem. 1183 * @hide 1184 */ 1185 public static @Nullable String sanitizeDisplayName(@Nullable String name) { 1186 return sanitizeDisplayName(name, /*rewriteHiddenFileName*/ false); 1187 } 1188 1189 /** 1190 * Sanitizes given name by appending '_' to make it non-hidden and mutating the file name to 1191 * make it valid for a FAT filesystem. 1192 * @hide 1193 */ 1194 public static @Nullable String sanitizeDisplayName(@Nullable String name, 1195 boolean rewriteHiddenFileName) { 1196 if (name == null) { 1197 return null; 1198 } else if (rewriteHiddenFileName && name.startsWith(".")) { 1199 // The resulting file must not be hidden. 1200 return "_" + name; 1201 } else { 1202 return buildValidFatFilename(name); 1203 } 1204 } 1205 1206 /** 1207 * Test if this given directory should be considered hidden. 1208 */ 1209 @VisibleForTesting 1210 public static boolean isDirectoryHidden(@NonNull File dir) { 1211 final String name = dir.getName(); 1212 if (name.startsWith(".")) { 1213 return true; 1214 } 1215 1216 final File nomedia = new File(dir, ".nomedia"); 1217 // check for .nomedia presence 1218 if (nomedia.exists()) { 1219 Logging.logPersistent("Observed non-standard " + nomedia); 1220 return true; 1221 } 1222 return false; 1223 } 1224 1225 /** 1226 * Test if this given file should be considered hidden. 1227 */ 1228 @VisibleForTesting 1229 public static boolean isFileHidden(@NonNull File file) { 1230 final String name = file.getName(); 1231 1232 // Handle well-known file names that are pending or trashed; they 1233 // normally appear hidden, but we give them special treatment 1234 if (PATTERN_EXPIRES_FILE.matcher(name).matches()) { 1235 return false; 1236 } 1237 1238 // Otherwise fall back to file name 1239 if (name.startsWith(".")) { 1240 return true; 1241 } 1242 return false; 1243 } 1244 1245 /** 1246 * Clears all app's external cache directories, i.e. for each app we delete 1247 * /sdcard/Android/data/app/cache/* but we keep the directory itself. 1248 * 1249 * @return 0 in case of success, or {@link OsConstants#EIO} if any error occurs. 1250 * 1251 * <p>This method doesn't perform any checks, so make sure that the calling package is allowed 1252 * to clear cache directories first. 1253 * 1254 * <p>If this method returned {@link OsConstants#EIO}, then we can't guarantee whether all, none 1255 * or part of the directories were cleared. 1256 */ 1257 public static int clearAppCacheDirectories() { 1258 int status = 0; 1259 Log.i(TAG, "Clearing cache for all apps"); 1260 final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(), 1261 "Android", "data"); 1262 for (File appDataDir : rootDataDir.listFiles()) { 1263 try { 1264 final File appCacheDir = new File(appDataDir, "cache"); 1265 if (appCacheDir.isDirectory()) { 1266 FileUtils.deleteContents(appCacheDir); 1267 } 1268 } catch (Exception e) { 1269 // We want to avoid crashing MediaProvider at all costs, so we handle all "generic" 1270 // exceptions here, and just report to the caller that an IO exception has occurred. 1271 // We still try to clear the rest of the directories. 1272 Log.e(TAG, "Couldn't delete all app cache dirs!", e); 1273 status = OsConstants.EIO; 1274 } 1275 } 1276 return status; 1277 } 1278 } 1279