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.content.pm.PackageManager; 49 import android.net.Uri; 50 import android.os.Environment; 51 import android.os.ParcelFileDescriptor; 52 import android.os.SystemProperties; 53 import android.os.UserHandle; 54 import android.os.storage.StorageManager; 55 import android.os.storage.StorageVolume; 56 import android.provider.MediaStore; 57 import android.provider.MediaStore.MediaColumns; 58 import android.system.ErrnoException; 59 import android.system.Os; 60 import android.system.OsConstants; 61 import android.text.TextUtils; 62 import android.text.format.DateUtils; 63 import android.util.Log; 64 import android.webkit.MimeTypeMap; 65 66 import androidx.annotation.NonNull; 67 import androidx.annotation.Nullable; 68 import androidx.annotation.VisibleForTesting; 69 70 import com.android.modules.utils.build.SdkLevel; 71 72 import java.io.File; 73 import java.io.FileDescriptor; 74 import java.io.FileNotFoundException; 75 import java.io.IOException; 76 import java.io.InputStream; 77 import java.io.OutputStream; 78 import java.nio.charset.StandardCharsets; 79 import java.nio.file.FileVisitResult; 80 import java.nio.file.FileVisitor; 81 import java.nio.file.Files; 82 import java.nio.file.NoSuchFileException; 83 import java.nio.file.Path; 84 import java.nio.file.attribute.BasicFileAttributes; 85 import java.util.ArrayList; 86 import java.util.Arrays; 87 import java.util.Collection; 88 import java.util.Comparator; 89 import java.util.Iterator; 90 import java.util.Locale; 91 import java.util.Objects; 92 import java.util.Optional; 93 import java.util.function.Consumer; 94 import java.util.regex.Matcher; 95 import java.util.regex.Pattern; 96 97 public class FileUtils { 98 // Even though vfat allows 255 UCS-2 chars, we might eventually write to 99 // ext4 through a FUSE layer, so use that limit. 100 @VisibleForTesting 101 static final int MAX_FILENAME_BYTES = 255; 102 103 /** 104 * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)} 105 * which adds security features like {@link OsConstants#O_CLOEXEC} and 106 * {@link OsConstants#O_NOFOLLOW}. 107 */ openSafely(@onNull File file, int pfdFlags)108 public static @NonNull ParcelFileDescriptor openSafely(@NonNull File file, int pfdFlags) 109 throws FileNotFoundException { 110 final int posixFlags = translateModePfdToPosix(pfdFlags) | O_CLOEXEC | O_NOFOLLOW; 111 try { 112 final FileDescriptor fd = Os.open(file.getAbsolutePath(), posixFlags, 113 S_IRWXU | S_IRWXG); 114 try { 115 return ParcelFileDescriptor.dup(fd); 116 } finally { 117 closeQuietly(fd); 118 } 119 } catch (IOException | ErrnoException e) { 120 throw new FileNotFoundException(e.getMessage()); 121 } 122 } 123 closeQuietly(@ullable AutoCloseable closeable)124 public static void closeQuietly(@Nullable AutoCloseable closeable) { 125 android.os.FileUtils.closeQuietly(closeable); 126 } 127 closeQuietly(@ullable FileDescriptor fd)128 public static void closeQuietly(@Nullable FileDescriptor fd) { 129 if (fd == null) return; 130 try { 131 Os.close(fd); 132 } catch (ErrnoException ignored) { 133 } 134 } 135 copy(@onNull InputStream in, @NonNull OutputStream out)136 public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { 137 return android.os.FileUtils.copy(in, out); 138 } 139 buildPath(File base, String... segments)140 public static File buildPath(File base, String... segments) { 141 File cur = base; 142 for (String segment : segments) { 143 if (cur == null) { 144 cur = new File(segment); 145 } else { 146 cur = new File(cur, segment); 147 } 148 } 149 return cur; 150 } 151 152 /** 153 * Delete older files in a directory until only those matching the given 154 * constraints remain. 155 * 156 * @param minCount Always keep at least this many files. 157 * @param minAgeMs Always keep files younger than this age, in milliseconds. 158 * @return if any files were deleted. 159 */ deleteOlderFiles(File dir, int minCount, long minAgeMs)160 public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) { 161 if (minCount < 0 || minAgeMs < 0) { 162 throw new IllegalArgumentException("Constraints must be positive or 0"); 163 } 164 165 final File[] files = dir.listFiles(); 166 if (files == null) return false; 167 168 // Sort with newest files first 169 Arrays.sort(files, new Comparator<File>() { 170 @Override 171 public int compare(File lhs, File rhs) { 172 return Long.compare(rhs.lastModified(), lhs.lastModified()); 173 } 174 }); 175 176 // Keep at least minCount files 177 boolean deleted = false; 178 for (int i = minCount; i < files.length; i++) { 179 final File file = files[i]; 180 181 // Keep files newer than minAgeMs 182 final long age = System.currentTimeMillis() - file.lastModified(); 183 if (age > minAgeMs) { 184 if (file.delete()) { 185 Log.d(TAG, "Deleted old file " + file); 186 deleted = true; 187 } 188 } 189 } 190 return deleted; 191 } 192 193 /** 194 * Shamelessly borrowed from {@code android.os.FileUtils}. 195 */ translateModeStringToPosix(String mode)196 public static int translateModeStringToPosix(String mode) { 197 // Sanity check for invalid chars 198 for (int i = 0; i < mode.length(); i++) { 199 switch (mode.charAt(i)) { 200 case 'r': 201 case 'w': 202 case 't': 203 case 'a': 204 break; 205 default: 206 throw new IllegalArgumentException("Bad mode: " + mode); 207 } 208 } 209 210 int res = 0; 211 if (mode.startsWith("rw")) { 212 res = O_RDWR | O_CREAT; 213 } else if (mode.startsWith("w")) { 214 res = O_WRONLY | O_CREAT; 215 } else if (mode.startsWith("r")) { 216 res = O_RDONLY; 217 } else { 218 throw new IllegalArgumentException("Bad mode: " + mode); 219 } 220 if (mode.indexOf('t') != -1) { 221 res |= O_TRUNC; 222 } 223 if (mode.indexOf('a') != -1) { 224 res |= O_APPEND; 225 } 226 return res; 227 } 228 229 /** 230 * Shamelessly borrowed from {@code android.os.FileUtils}. 231 */ translateModePosixToString(int mode)232 public static String translateModePosixToString(int mode) { 233 String res = ""; 234 if ((mode & O_ACCMODE) == O_RDWR) { 235 res = "rw"; 236 } else if ((mode & O_ACCMODE) == O_WRONLY) { 237 res = "w"; 238 } else if ((mode & O_ACCMODE) == O_RDONLY) { 239 res = "r"; 240 } else { 241 throw new IllegalArgumentException("Bad mode: " + mode); 242 } 243 if ((mode & O_TRUNC) == O_TRUNC) { 244 res += "t"; 245 } 246 if ((mode & O_APPEND) == O_APPEND) { 247 res += "a"; 248 } 249 return res; 250 } 251 252 /** 253 * Shamelessly borrowed from {@code android.os.FileUtils}. 254 */ translateModePosixToPfd(int mode)255 public static int translateModePosixToPfd(int mode) { 256 int res = 0; 257 if ((mode & O_ACCMODE) == O_RDWR) { 258 res = MODE_READ_WRITE; 259 } else if ((mode & O_ACCMODE) == O_WRONLY) { 260 res = MODE_WRITE_ONLY; 261 } else if ((mode & O_ACCMODE) == O_RDONLY) { 262 res = MODE_READ_ONLY; 263 } else { 264 throw new IllegalArgumentException("Bad mode: " + mode); 265 } 266 if ((mode & O_CREAT) == O_CREAT) { 267 res |= MODE_CREATE; 268 } 269 if ((mode & O_TRUNC) == O_TRUNC) { 270 res |= MODE_TRUNCATE; 271 } 272 if ((mode & O_APPEND) == O_APPEND) { 273 res |= MODE_APPEND; 274 } 275 return res; 276 } 277 278 /** 279 * Shamelessly borrowed from {@code android.os.FileUtils}. 280 */ translateModePfdToPosix(int mode)281 public static int translateModePfdToPosix(int mode) { 282 int res = 0; 283 if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) { 284 res = O_RDWR; 285 } else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) { 286 res = O_WRONLY; 287 } else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) { 288 res = O_RDONLY; 289 } else { 290 throw new IllegalArgumentException("Bad mode: " + mode); 291 } 292 if ((mode & MODE_CREATE) == MODE_CREATE) { 293 res |= O_CREAT; 294 } 295 if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) { 296 res |= O_TRUNC; 297 } 298 if ((mode & MODE_APPEND) == MODE_APPEND) { 299 res |= O_APPEND; 300 } 301 return res; 302 } 303 304 /** 305 * Shamelessly borrowed from {@code android.os.FileUtils}. 306 */ translateModeAccessToPosix(int mode)307 public static int translateModeAccessToPosix(int mode) { 308 if (mode == F_OK) { 309 // There's not an exact mapping, so we attempt a read-only open to 310 // determine if a file exists 311 return O_RDONLY; 312 } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) { 313 return O_RDWR; 314 } else if ((mode & R_OK) == R_OK) { 315 return O_RDONLY; 316 } else if ((mode & W_OK) == W_OK) { 317 return O_WRONLY; 318 } else { 319 throw new IllegalArgumentException("Bad mode: " + mode); 320 } 321 } 322 323 /** 324 * Test if a file lives under the given directory, either as a direct child 325 * or a distant grandchild. 326 * <p> 327 * Both files <em>must</em> have been resolved using 328 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 329 * attacks. 330 * 331 * @hide 332 */ contains(File[] dirs, File file)333 public static boolean contains(File[] dirs, File file) { 334 for (File dir : dirs) { 335 if (contains(dir, file)) { 336 return true; 337 } 338 } 339 return false; 340 } 341 342 /** {@hide} */ contains(Collection<File> dirs, File file)343 public static boolean contains(Collection<File> dirs, File file) { 344 for (File dir : dirs) { 345 if (contains(dir, file)) { 346 return true; 347 } 348 } 349 return false; 350 } 351 352 /** 353 * Test if a file lives under the given directory, either as a direct child 354 * or a distant grandchild. 355 * <p> 356 * Both files <em>must</em> have been resolved using 357 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 358 * attacks. 359 * 360 * @hide 361 */ contains(File dir, File file)362 public static boolean contains(File dir, File file) { 363 if (dir == null || file == null) return false; 364 return contains(dir.getAbsolutePath(), file.getAbsolutePath()); 365 } 366 367 /** 368 * Test if a file lives under the given directory, either as a direct child 369 * or a distant grandchild. 370 * <p> 371 * Both files <em>must</em> have been resolved using 372 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 373 * attacks. 374 * 375 * @hide 376 */ contains(String dirPath, String filePath)377 public static boolean contains(String dirPath, String filePath) { 378 if (dirPath.equals(filePath)) { 379 return true; 380 } 381 if (!dirPath.endsWith("/")) { 382 dirPath += "/"; 383 } 384 return filePath.startsWith(dirPath); 385 } 386 387 /** 388 * Write {@link String} to the given {@link File}. Deletes any existing file 389 * when the argument is {@link Optional#empty()}. 390 */ writeString(@onNull File file, @NonNull Optional<String> value)391 public static void writeString(@NonNull File file, @NonNull Optional<String> value) 392 throws IOException { 393 if (value.isPresent()) { 394 Files.write(file.toPath(), value.get().getBytes(StandardCharsets.UTF_8)); 395 } else { 396 file.delete(); 397 } 398 } 399 400 private static final int MAX_READ_STRING_SIZE = 4096; 401 402 /** 403 * Read given {@link File} as a single {@link String}. Returns 404 * {@link Optional#empty()} when 405 * <ul> 406 * <li> the file doesn't exist or 407 * <li> the size of the file exceeds {@code MAX_READ_STRING_SIZE} 408 * </ul> 409 */ readString(@onNull File file)410 public static @NonNull Optional<String> readString(@NonNull File file) throws IOException { 411 try { 412 if (file.length() <= MAX_READ_STRING_SIZE) { 413 final String value = new String(Files.readAllBytes(file.toPath()), 414 StandardCharsets.UTF_8); 415 return Optional.of(value); 416 } 417 // When file size exceeds MAX_READ_STRING_SIZE, file is either 418 // corrupted or doesn't the contain expected data. Hence we return 419 // Optional.empty() which will be interpreted as empty file. 420 Logging.logPersistent(String.format("Ignored reading %s, file size exceeds %d", file, 421 MAX_READ_STRING_SIZE)); 422 } catch (NoSuchFileException ignored) { 423 } 424 return Optional.empty(); 425 } 426 427 /** 428 * Recursively walk the contents of the given {@link Path}, invoking the 429 * given {@link Consumer} for every file and directory encountered. This is 430 * typically used for recursively deleting a directory tree. 431 * <p> 432 * Gracefully attempts to process as much as possible in the face of any 433 * failures. 434 */ walkFileTreeContents(@onNull Path path, @NonNull Consumer<Path> operation)435 public static void walkFileTreeContents(@NonNull Path path, @NonNull Consumer<Path> operation) { 436 try { 437 Files.walkFileTree(path, new FileVisitor<Path>() { 438 @Override 439 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { 440 return FileVisitResult.CONTINUE; 441 } 442 443 @Override 444 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 445 if (!Objects.equals(path, file)) { 446 operation.accept(file); 447 } 448 return FileVisitResult.CONTINUE; 449 } 450 451 @Override 452 public FileVisitResult visitFileFailed(Path file, IOException e) { 453 Log.w(TAG, "Failed to visit " + file, e); 454 return FileVisitResult.CONTINUE; 455 } 456 457 @Override 458 public FileVisitResult postVisitDirectory(Path dir, IOException e) { 459 if (!Objects.equals(path, dir)) { 460 operation.accept(dir); 461 } 462 return FileVisitResult.CONTINUE; 463 } 464 }); 465 } catch (IOException e) { 466 Log.w(TAG, "Failed to walk " + path, e); 467 } 468 } 469 470 /** 471 * Recursively delete all contents inside the given directory. Gracefully 472 * attempts to delete as much as possible in the face of any failures. 473 * 474 * @deprecated if you're calling this from inside {@code MediaProvider}, you 475 * likely want to call {@link #forEach} with a separate 476 * invocation to invalidate FUSE entries. 477 */ 478 @Deprecated deleteContents(@onNull File dir)479 public static void deleteContents(@NonNull File dir) { 480 walkFileTreeContents(dir.toPath(), (path) -> { 481 path.toFile().delete(); 482 }); 483 } 484 isValidFatFilenameChar(char c)485 private static boolean isValidFatFilenameChar(char c) { 486 if ((0x00 <= c && c <= 0x1f)) { 487 return false; 488 } 489 switch (c) { 490 case '"': 491 case '*': 492 case '/': 493 case ':': 494 case '<': 495 case '>': 496 case '?': 497 case '\\': 498 case '|': 499 case 0x7F: 500 return false; 501 default: 502 return true; 503 } 504 } 505 506 /** 507 * Check if given filename is valid for a FAT filesystem. 508 * 509 * @hide 510 */ isValidFatFilename(String name)511 public static boolean isValidFatFilename(String name) { 512 return (name != null) && name.equals(buildValidFatFilename(name)); 513 } 514 515 /** 516 * Mutate the given filename to make it valid for a FAT filesystem, 517 * replacing any invalid characters with "_". 518 * 519 * @hide 520 */ buildValidFatFilename(String name)521 public static String buildValidFatFilename(String name) { 522 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 523 return "(invalid)"; 524 } 525 final StringBuilder res = new StringBuilder(name.length()); 526 for (int i = 0; i < name.length(); i++) { 527 final char c = name.charAt(i); 528 if (isValidFatFilenameChar(c)) { 529 res.append(c); 530 } else { 531 res.append('_'); 532 } 533 } 534 535 trimFilename(res, MAX_FILENAME_BYTES); 536 return res.toString(); 537 } 538 539 /** {@hide} */ 540 // @VisibleForTesting trimFilename(String str, int maxBytes)541 public static String trimFilename(String str, int maxBytes) { 542 final StringBuilder res = new StringBuilder(str); 543 trimFilename(res, maxBytes); 544 return res.toString(); 545 } 546 547 /** {@hide} */ trimFilename(StringBuilder res, int maxBytes)548 private static void trimFilename(StringBuilder res, int maxBytes) { 549 byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8); 550 if (raw.length > maxBytes) { 551 maxBytes -= 3; 552 while (raw.length > maxBytes) { 553 res.deleteCharAt(res.length() / 2); 554 raw = res.toString().getBytes(StandardCharsets.UTF_8); 555 } 556 res.insert(res.length() / 2, "..."); 557 } 558 } 559 560 /** {@hide} */ buildUniqueFileWithExtension(File parent, String name, String ext)561 private static File buildUniqueFileWithExtension(File parent, String name, String ext) 562 throws FileNotFoundException { 563 final Iterator<String> names = buildUniqueNameIterator(parent, name); 564 while (names.hasNext()) { 565 File file = buildFile(parent, names.next(), ext); 566 if (!file.exists()) { 567 return file; 568 } 569 } 570 throw new FileNotFoundException("Failed to create unique file"); 571 } 572 573 private static final Pattern PATTERN_DCF_STRICT = Pattern 574 .compile("([A-Z0-9_]{4})([0-9]{4})"); 575 private static final Pattern PATTERN_DCF_RELAXED = Pattern 576 .compile("((?:IMG|MVIMG|VID)_[0-9]{8}_[0-9]{6})(?:~([0-9]+))?"); 577 isDcim(@onNull File dir)578 private static boolean isDcim(@NonNull File dir) { 579 while (dir != null) { 580 if (Objects.equals("DCIM", dir.getName())) { 581 return true; 582 } 583 dir = dir.getParentFile(); 584 } 585 return false; 586 } 587 buildUniqueNameIterator(@onNull File parent, @NonNull String name)588 private static @NonNull Iterator<String> buildUniqueNameIterator(@NonNull File parent, 589 @NonNull String name) { 590 if (isDcim(parent)) { 591 final Matcher dcfStrict = PATTERN_DCF_STRICT.matcher(name); 592 if (dcfStrict.matches()) { 593 // Generate names like "IMG_1001" 594 final String prefix = dcfStrict.group(1); 595 return new Iterator<String>() { 596 int i = Integer.parseInt(dcfStrict.group(2)); 597 @Override 598 public String next() { 599 final String res = String.format(Locale.US, "%s%04d", prefix, i); 600 i++; 601 return res; 602 } 603 @Override 604 public boolean hasNext() { 605 return i <= 9999; 606 } 607 }; 608 } 609 610 final Matcher dcfRelaxed = PATTERN_DCF_RELAXED.matcher(name); 611 if (dcfRelaxed.matches()) { 612 // Generate names like "IMG_20190102_030405~2" 613 final String prefix = dcfRelaxed.group(1); 614 return new Iterator<String>() { 615 int i = TextUtils.isEmpty(dcfRelaxed.group(2)) 616 ? 1 617 : Integer.parseInt(dcfRelaxed.group(2)); 618 @Override 619 public String next() { 620 final String res = (i == 1) 621 ? prefix 622 : String.format(Locale.US, "%s~%d", prefix, i); 623 i++; 624 return res; 625 } 626 @Override 627 public boolean hasNext() { 628 return i <= 99; 629 } 630 }; 631 } 632 } 633 634 // Generate names like "foo (2)" 635 return new Iterator<String>() { 636 int i = 0; 637 @Override 638 public String next() { 639 final String res = (i == 0) ? name : name + " (" + i + ")"; 640 i++; 641 return res; 642 } 643 @Override 644 public boolean hasNext() { 645 return i < 32; 646 } 647 }; 648 } 649 650 /** 651 * Generates a unique file name under the given parent directory. If the display name doesn't 652 * have an extension that matches the requested MIME type, the default extension for that MIME 653 * type is appended. If a file already exists, the name is appended with a numerical value to 654 * make it unique. 655 * 656 * For example, the display name 'example' with 'text/plain' MIME might produce 657 * 'example.txt' or 'example (1).txt', etc. 658 * 659 * @throws FileNotFoundException 660 * @hide 661 */ 662 public static File buildUniqueFile(File parent, String mimeType, String displayName) 663 throws FileNotFoundException { 664 final String[] parts = splitFileName(mimeType, displayName); 665 return buildUniqueFileWithExtension(parent, parts[0], parts[1]); 666 } 667 668 /** {@hide} */ 669 public static File buildNonUniqueFile(File parent, String mimeType, String displayName) { 670 final String[] parts = splitFileName(mimeType, displayName); 671 return buildFile(parent, parts[0], parts[1]); 672 } 673 674 /** 675 * Generates a unique file name under the given parent directory, keeping 676 * any extension intact. 677 * 678 * @hide 679 */ 680 public static File buildUniqueFile(File parent, String displayName) 681 throws FileNotFoundException { 682 final String name; 683 final String ext; 684 685 // Extract requested extension from display name 686 final int lastDot = displayName.lastIndexOf('.'); 687 if (lastDot >= 0) { 688 name = displayName.substring(0, lastDot); 689 ext = displayName.substring(lastDot + 1); 690 } else { 691 name = displayName; 692 ext = null; 693 } 694 695 return buildUniqueFileWithExtension(parent, name, ext); 696 } 697 698 /** 699 * Splits file name into base name and extension. 700 * If the display name doesn't have an extension that matches the requested MIME type, the 701 * extension is regarded as a part of filename and default extension for that MIME type is 702 * appended. 703 * 704 * @hide 705 */ 706 public static String[] splitFileName(String mimeType, String displayName) { 707 String name; 708 String ext; 709 710 { 711 String mimeTypeFromExt; 712 713 // Extract requested extension from display name 714 final int lastDot = displayName.lastIndexOf('.'); 715 if (lastDot > 0) { 716 name = displayName.substring(0, lastDot); 717 ext = displayName.substring(lastDot + 1); 718 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 719 ext.toLowerCase(Locale.ROOT)); 720 } else { 721 name = displayName; 722 ext = null; 723 mimeTypeFromExt = null; 724 } 725 726 if (mimeTypeFromExt == null) { 727 mimeTypeFromExt = ClipDescription.MIMETYPE_UNKNOWN; 728 } 729 730 final String extFromMimeType; 731 if (ClipDescription.MIMETYPE_UNKNOWN.equalsIgnoreCase(mimeType)) { 732 extFromMimeType = null; 733 } else { 734 extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 735 } 736 737 if (StringUtils.equalIgnoreCase(mimeType, mimeTypeFromExt) 738 || StringUtils.equalIgnoreCase(ext, extFromMimeType)) { 739 // Extension maps back to requested MIME type; allow it 740 } else { 741 // No match; insist that create file matches requested MIME 742 name = displayName; 743 ext = extFromMimeType; 744 } 745 } 746 747 if (ext == null) { 748 ext = ""; 749 } 750 751 return new String[] { name, ext }; 752 } 753 754 /** {@hide} */ 755 private static File buildFile(File parent, String name, String ext) { 756 if (TextUtils.isEmpty(ext)) { 757 return new File(parent, name); 758 } else { 759 return new File(parent, name + "." + ext); 760 } 761 } 762 763 public static @Nullable String extractDisplayName(@Nullable String data) { 764 if (data == null) return null; 765 if (data.indexOf('/') == -1) { 766 return data; 767 } 768 if (data.endsWith("/")) { 769 data = data.substring(0, data.length() - 1); 770 } 771 return data.substring(data.lastIndexOf('/') + 1); 772 } 773 774 public static @Nullable String extractFileName(@Nullable String data) { 775 if (data == null) return null; 776 data = extractDisplayName(data); 777 778 final int lastDot = data.lastIndexOf('.'); 779 if (lastDot == -1) { 780 return data; 781 } else { 782 return data.substring(0, lastDot); 783 } 784 } 785 786 public static @Nullable String extractFileExtension(@Nullable String data) { 787 if (data == null) return null; 788 data = extractDisplayName(data); 789 790 final int lastDot = data.lastIndexOf('.'); 791 if (lastDot == -1) { 792 return null; 793 } else { 794 return data.substring(lastDot + 1); 795 } 796 } 797 798 /** 799 * Return list of paths that should be scanned with 800 * {@link com.android.providers.media.scan.MediaScanner} for the given 801 * volume name. 802 */ 803 public static @NonNull Collection<File> getVolumeScanPaths(@NonNull Context context, 804 @NonNull String volumeName) throws FileNotFoundException { 805 final ArrayList<File> res = new ArrayList<>(); 806 switch (volumeName) { 807 case MediaStore.VOLUME_INTERNAL: { 808 res.addAll(Environment.getInternalMediaDirectories()); 809 break; 810 } 811 case MediaStore.VOLUME_EXTERNAL: { 812 for (String resolvedVolumeName : MediaStore.getExternalVolumeNames(context)) { 813 res.add(getVolumePath(context, resolvedVolumeName)); 814 } 815 break; 816 } 817 default: { 818 res.add(getVolumePath(context, volumeName)); 819 } 820 } 821 return res; 822 } 823 824 /** 825 * Return path where the given volume name is mounted. 826 */ 827 public static @NonNull File getVolumePath(@NonNull Context context, 828 @NonNull String volumeName) throws FileNotFoundException { 829 switch (volumeName) { 830 case MediaStore.VOLUME_INTERNAL: 831 case MediaStore.VOLUME_EXTERNAL: 832 throw new FileNotFoundException(volumeName + " has no associated path"); 833 } 834 835 final Uri uri = MediaStore.Files.getContentUri(volumeName); 836 File path = null; 837 838 try { 839 path = context.getSystemService(StorageManager.class).getStorageVolume(uri) 840 .getDirectory(); 841 } catch (IllegalStateException e) { 842 Log.w("Ignoring volume not found exception", e); 843 } 844 845 if (path != null) { 846 return path; 847 } else { 848 throw new FileNotFoundException(volumeName + " has no associated path"); 849 } 850 } 851 852 /** 853 * Returns the content URI for the volume that contains the given path. 854 * 855 * <p>{@link MediaStore.Files#getContentUriForPath(String)} can't detect public volumes and can 856 * only return the URI for the primary external storage, that's why this utility should be used 857 * instead. 858 */ 859 public static @NonNull Uri getContentUriForPath(@NonNull String path) { 860 Objects.requireNonNull(path); 861 return MediaStore.Files.getContentUri(extractVolumeName(path)); 862 } 863 864 /** 865 * Return StorageVolume corresponding to the file on Path 866 */ 867 public static @NonNull StorageVolume getStorageVolume(@NonNull Context context, 868 @NonNull File path) throws FileNotFoundException { 869 int userId = extractUserId(path.getPath()); 870 Context userContext = context; 871 if (userId >= 0 && (context.getUser().getIdentifier() != userId)) { 872 // This volume is for a different user than our context, create a context 873 // for that user to retrieve the correct volume. 874 try { 875 userContext = context.createPackageContextAsUser("system", 0, 876 UserHandle.of(userId)); 877 } catch (PackageManager.NameNotFoundException e) { 878 throw new FileNotFoundException("Can't get package context for user " + userId); 879 } 880 } 881 882 StorageVolume volume = userContext.getSystemService(StorageManager.class) 883 .getStorageVolume(path); 884 if (volume == null) { 885 throw new FileNotFoundException("Can't find volume for " + path.getPath()); 886 } 887 888 return volume; 889 } 890 891 /** 892 * Return volume name which hosts the given path. 893 */ 894 public static @NonNull String getVolumeName(@NonNull Context context, @NonNull File path) 895 throws FileNotFoundException { 896 if (contains(Environment.getStorageDirectory(), path)) { 897 StorageVolume volume = getStorageVolume(context, path); 898 return volume.getMediaStoreVolumeName(); 899 } else { 900 return MediaStore.VOLUME_INTERNAL; 901 } 902 } 903 904 public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile( 905 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/.+"); 906 public static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile( 907 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/?"); 908 public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile( 909 "(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$"); 910 public static final Pattern PATTERN_PENDING_FILEPATH_FOR_SQL = Pattern.compile( 911 ".*/\\.pending-(\\d+)-([^/]+)$"); 912 913 /** 914 * File prefix indicating that the file {@link MediaColumns#IS_PENDING}. 915 */ 916 public static final String PREFIX_PENDING = "pending"; 917 918 /** 919 * File prefix indicating that the file {@link MediaColumns#IS_TRASHED}. 920 */ 921 public static final String PREFIX_TRASHED = "trashed"; 922 923 /** 924 * Default duration that {@link MediaColumns#IS_PENDING} items should be 925 * preserved for until automatically cleaned by {@link #runIdleMaintenance}. 926 */ 927 public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS; 928 929 /** 930 * Default duration that {@link MediaColumns#IS_TRASHED} items should be 931 * preserved for until automatically cleaned by {@link #runIdleMaintenance}. 932 */ 933 public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS; 934 935 /** 936 * Default duration that expired items should be extended in 937 * {@link #runIdleMaintenance}. 938 */ 939 public static final long DEFAULT_DURATION_EXTENDED = 7 * DateUtils.DAY_IN_MILLIS; 940 941 public static boolean isDownload(@NonNull String path) { 942 return PATTERN_DOWNLOADS_FILE.matcher(path).matches(); 943 } 944 945 public static boolean isDownloadDir(@NonNull String path) { 946 return PATTERN_DOWNLOADS_DIRECTORY.matcher(path).matches(); 947 } 948 949 private static final boolean PROP_CROSS_USER_ALLOWED = 950 SystemProperties.getBoolean("external_storage.cross_user.enabled", false); 951 952 private static final String PROP_CROSS_USER_ROOT = isCrossUserEnabled() 953 ? SystemProperties.get("external_storage.cross_user.root", "") : ""; 954 955 private static final String PROP_CROSS_USER_ROOT_PATTERN = ((PROP_CROSS_USER_ROOT.isEmpty()) 956 ? "" : "(?:" + PROP_CROSS_USER_ROOT + "/)?"); 957 958 /** 959 * Regex that matches paths in all well-known package-specific directories, 960 * and which captures the package name as the first group. 961 */ 962 public static final Pattern PATTERN_OWNED_PATH = Pattern.compile( 963 "(?i)^/storage/[^/]+/(?:[0-9]+/)?" 964 + PROP_CROSS_USER_ROOT_PATTERN 965 + "Android/(?:data|media|obb)/([^/]+)(/?.*)?"); 966 967 /** 968 * Regex that matches paths in all well-known package-specific relative directory 969 * path (as defined in {@link MediaColumns#RELATIVE_PATH}) 970 * and which captures the package name as the first group. 971 */ 972 private static final Pattern PATTERN_OWNED_RELATIVE_PATH = Pattern.compile( 973 "(?i)^Android/(?:data|media|obb)/([^/]+)(/?.*)?"); 974 975 /** 976 * Regex that matches exactly Android/obb or Android/data or Android/obb/ or Android/data/ 977 * suffix absolute file path. 978 */ 979 private static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile( 980 "(?i)^/storage/[^/]+/(?:[0-9]+/)?" 981 + PROP_CROSS_USER_ROOT_PATTERN 982 + "Android/(?:data|obb)/?$"); 983 984 /** 985 * Regex that matches Android/obb or Android/data relative path (as defined in 986 * {@link MediaColumns#RELATIVE_PATH}) 987 */ 988 private static final Pattern PATTERN_DATA_OR_OBB_RELATIVE_PATH = Pattern.compile( 989 "(?i)^Android/(?:data|obb)(?:/.*)?$"); 990 991 /** 992 * Regex that matches Android/obb {@link MediaColumns#RELATIVE_PATH}. 993 */ 994 private static final Pattern PATTERN_OBB_OR_CHILD_RELATIVE_PATH = Pattern.compile( 995 "(?i)^Android/obb(?:/.*)?$"); 996 997 private static final Pattern PATTERN_VISIBLE = Pattern.compile( 998 "(?i)^/storage/[^/]+(?:/[0-9]+)?$"); 999 1000 private static final Pattern PATTERN_INVISIBLE = Pattern.compile( 1001 "(?i)^/storage/[^/]+(?:/[0-9]+)?/" 1002 + "(?:(?:Android/(?:data|obb|sandbox)$)|" 1003 + "(?:\\.transforms$)|" 1004 + "(?:(?:Movies|Music|Pictures)/.thumbnails$))"); 1005 1006 /** 1007 * The recordings directory. This is used for R OS. For S OS or later, 1008 * we use {@link Environment#DIRECTORY_RECORDINGS} directly. 1009 */ 1010 public static final String DIRECTORY_RECORDINGS = "Recordings"; 1011 1012 @VisibleForTesting 1013 public static final String[] DEFAULT_FOLDER_NAMES; 1014 static { 1015 if (SdkLevel.isAtLeastS()) { 1016 DEFAULT_FOLDER_NAMES = new String[]{ 1017 Environment.DIRECTORY_MUSIC, 1018 Environment.DIRECTORY_PODCASTS, 1019 Environment.DIRECTORY_RINGTONES, 1020 Environment.DIRECTORY_ALARMS, 1021 Environment.DIRECTORY_NOTIFICATIONS, 1022 Environment.DIRECTORY_PICTURES, 1023 Environment.DIRECTORY_MOVIES, 1024 Environment.DIRECTORY_DOWNLOADS, 1025 Environment.DIRECTORY_DCIM, 1026 Environment.DIRECTORY_DOCUMENTS, 1027 Environment.DIRECTORY_AUDIOBOOKS, 1028 Environment.DIRECTORY_RECORDINGS, 1029 }; 1030 } else { 1031 DEFAULT_FOLDER_NAMES = new String[]{ 1032 Environment.DIRECTORY_MUSIC, 1033 Environment.DIRECTORY_PODCASTS, 1034 Environment.DIRECTORY_RINGTONES, 1035 Environment.DIRECTORY_ALARMS, 1036 Environment.DIRECTORY_NOTIFICATIONS, 1037 Environment.DIRECTORY_PICTURES, 1038 Environment.DIRECTORY_MOVIES, 1039 Environment.DIRECTORY_DOWNLOADS, 1040 Environment.DIRECTORY_DCIM, 1041 Environment.DIRECTORY_DOCUMENTS, 1042 Environment.DIRECTORY_AUDIOBOOKS, 1043 DIRECTORY_RECORDINGS, 1044 }; 1045 } 1046 } 1047 1048 /** 1049 * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH} 1050 */ 1051 private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile( 1052 "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)"); 1053 1054 /** 1055 * Regex that matches paths under well-known storage paths. 1056 */ 1057 private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile( 1058 "(?i)^/storage/([^/]+)"); 1059 1060 /** 1061 * Regex that matches user-ids under well-known storage paths. 1062 */ 1063 private static final Pattern PATTERN_USER_ID = Pattern.compile( 1064 "(?i)^/storage/emulated/([0-9]+)"); 1065 1066 private static final String CAMERA_RELATIVE_PATH = 1067 String.format("%s/%s/", Environment.DIRECTORY_DCIM, "Camera"); 1068 1069 public static boolean isCrossUserEnabled() { 1070 return PROP_CROSS_USER_ALLOWED || SdkLevel.isAtLeastS(); 1071 } 1072 1073 private static @Nullable String normalizeUuid(@Nullable String fsUuid) { 1074 return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null; 1075 } 1076 1077 public static int extractUserId(@Nullable String data) { 1078 if (data == null) return -1; 1079 final Matcher matcher = PATTERN_USER_ID.matcher(data); 1080 if (matcher.find()) { 1081 return Integer.parseInt(matcher.group(1)); 1082 } 1083 1084 return -1; 1085 } 1086 1087 public static @Nullable String extractVolumePath(@Nullable String data) { 1088 if (data == null) return null; 1089 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 1090 if (matcher.find()) { 1091 return data.substring(0, matcher.end()); 1092 } else { 1093 return null; 1094 } 1095 } 1096 1097 public static @Nullable String extractVolumeName(@Nullable String data) { 1098 if (data == null) return null; 1099 final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data); 1100 if (matcher.find()) { 1101 final String volumeName = matcher.group(1); 1102 if (volumeName.equals("emulated")) { 1103 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 1104 } else { 1105 return normalizeUuid(volumeName); 1106 } 1107 } else { 1108 return MediaStore.VOLUME_INTERNAL; 1109 } 1110 } 1111 1112 public static @Nullable String extractRelativePath(@Nullable String data) { 1113 data = getCanonicalPath(data); 1114 if (data == null) return null; 1115 1116 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 1117 if (matcher.find()) { 1118 final int lastSlash = data.lastIndexOf('/'); 1119 if (lastSlash == -1 || lastSlash < matcher.end()) { 1120 // This is a file in the top-level directory, so relative path is "/" 1121 // which is different than null, which means unknown path 1122 return "/"; 1123 } else { 1124 return data.substring(matcher.end(), lastSlash + 1); 1125 } 1126 } else { 1127 return null; 1128 } 1129 } 1130 1131 /** 1132 * Returns relative path with display name. 1133 */ 1134 @VisibleForTesting 1135 public static @Nullable String extractRelativePathWithDisplayName(@Nullable String path) { 1136 if (path == null) return null; 1137 1138 if (path.equals("/storage/emulated") || path.equals("/storage/emulated/")) { 1139 // This path is not reachable for MediaProvider. 1140 return null; 1141 } 1142 1143 // We are extracting relative path for the directory itself, we add "/" so that we can use 1144 // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative 1145 // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for 1146 // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/". 1147 if (!path.endsWith("/")) { 1148 // Relative path for directory should end with "/". 1149 path += "/"; 1150 } 1151 1152 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path); 1153 if (matcher.find()) { 1154 if (matcher.end() == path.length()) { 1155 // This is the top-level directory, so relative path is "/" 1156 return "/"; 1157 } 1158 return path.substring(matcher.end()); 1159 } 1160 return null; 1161 } 1162 1163 public static @Nullable String extractPathOwnerPackageName(@Nullable String path) { 1164 if (path == null) return null; 1165 final Matcher m = PATTERN_OWNED_PATH.matcher(path); 1166 if (m.matches()) { 1167 return m.group(1); 1168 } 1169 return null; 1170 } 1171 1172 public static @Nullable String extractOwnerPackageNameFromRelativePath(@Nullable String path) { 1173 if (path == null) return null; 1174 final Matcher m = PATTERN_OWNED_RELATIVE_PATH.matcher(path); 1175 if (m.matches()) { 1176 return m.group(1); 1177 } 1178 return null; 1179 } 1180 1181 public static boolean isExternalMediaDirectory(@NonNull String path) { 1182 return isExternalMediaDirectory(path, PROP_CROSS_USER_ROOT); 1183 } 1184 1185 @VisibleForTesting 1186 static boolean isExternalMediaDirectory(@NonNull String path, String crossUserRoot) { 1187 final String relativePath = extractRelativePath(path); 1188 if (relativePath == null) { 1189 return false; 1190 } 1191 1192 if (StringUtils.startsWithIgnoreCase(relativePath, "Android/media")) { 1193 return true; 1194 } 1195 if (!TextUtils.isEmpty(crossUserRoot)) { 1196 return StringUtils.startsWithIgnoreCase(relativePath, crossUserRoot + "/Android/media"); 1197 } 1198 1199 return false; 1200 } 1201 1202 /** 1203 * Returns true if path is Android/data or Android/obb path. 1204 */ 1205 public static boolean isDataOrObbPath(@Nullable String path) { 1206 if (path == null) return false; 1207 final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path); 1208 return m.matches(); 1209 } 1210 1211 /** 1212 * Returns true if relative path is Android/data or Android/obb path. 1213 */ 1214 public static boolean isDataOrObbRelativePath(@Nullable String path) { 1215 if (path == null) return false; 1216 final Matcher m = PATTERN_DATA_OR_OBB_RELATIVE_PATH.matcher(path); 1217 return m.matches(); 1218 } 1219 1220 /** 1221 * Returns true if relative path is Android/obb path. 1222 */ 1223 public static boolean isObbOrChildRelativePath(@Nullable String path) { 1224 if (path == null) return false; 1225 final Matcher m = PATTERN_OBB_OR_CHILD_RELATIVE_PATH.matcher(path); 1226 return m.matches(); 1227 } 1228 1229 public static boolean shouldBeVisible(@Nullable String path) { 1230 if (path == null) return false; 1231 final Matcher m = PATTERN_VISIBLE.matcher(path); 1232 return m.matches(); 1233 } 1234 1235 public static boolean shouldBeInvisible(@Nullable String path) { 1236 if (path == null) return false; 1237 final Matcher m = PATTERN_INVISIBLE.matcher(path); 1238 return m.matches(); 1239 } 1240 1241 /** 1242 * Returns the name of the top level directory, or null if the path doesn't go through the 1243 * external storage directory. 1244 */ 1245 @Nullable 1246 public static String extractTopLevelDir(String path) { 1247 final String relativePath = extractRelativePath(path); 1248 if (relativePath == null) { 1249 return null; 1250 } 1251 1252 return extractTopLevelDir(relativePath.split("/")); 1253 } 1254 1255 @Nullable 1256 public static String extractTopLevelDir(String[] relativePathSegments) { 1257 return extractTopLevelDir(relativePathSegments, PROP_CROSS_USER_ROOT); 1258 } 1259 1260 @VisibleForTesting 1261 @Nullable 1262 static String extractTopLevelDir(String[] relativePathSegments, String crossUserRoot) { 1263 if (relativePathSegments == null) return null; 1264 1265 final String topLevelDir = relativePathSegments.length > 0 ? relativePathSegments[0] : null; 1266 if (crossUserRoot != null && crossUserRoot.equals(topLevelDir)) { 1267 return relativePathSegments.length > 1 ? relativePathSegments[1] : null; 1268 } 1269 1270 return topLevelDir; 1271 } 1272 1273 public static boolean isDefaultDirectoryName(@Nullable String dirName) { 1274 for (String defaultDirName : DEFAULT_FOLDER_NAMES) { 1275 if (defaultDirName.equalsIgnoreCase(dirName)) { 1276 return true; 1277 } 1278 } 1279 return false; 1280 } 1281 1282 /** 1283 * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other 1284 * columns being modified by this operation. 1285 */ 1286 public static void computeDateExpires(@NonNull ContentValues values) { 1287 // External apps have no ability to change this field 1288 values.remove(MediaColumns.DATE_EXPIRES); 1289 1290 // Only define the field when this modification is actually adjusting 1291 // one of the flags that should influence the expiration 1292 final Object pending = values.get(MediaColumns.IS_PENDING); 1293 if (pending != null) { 1294 if (parseBoolean(pending, false)) { 1295 values.put(MediaColumns.DATE_EXPIRES, 1296 (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000); 1297 } else { 1298 values.putNull(MediaColumns.DATE_EXPIRES); 1299 } 1300 } 1301 final Object trashed = values.get(MediaColumns.IS_TRASHED); 1302 if (trashed != null) { 1303 if (parseBoolean(trashed, false)) { 1304 values.put(MediaColumns.DATE_EXPIRES, 1305 (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000); 1306 } else { 1307 values.putNull(MediaColumns.DATE_EXPIRES); 1308 } 1309 } 1310 } 1311 1312 /** 1313 * Compute several scattered {@link MediaColumns} values from 1314 * {@link MediaColumns#DATA}. This method performs no enforcement of 1315 * argument validity. 1316 */ 1317 public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) { 1318 // Worst case we have to assume no bucket details 1319 values.remove(MediaColumns.VOLUME_NAME); 1320 values.remove(MediaColumns.RELATIVE_PATH); 1321 values.remove(MediaColumns.IS_TRASHED); 1322 values.remove(MediaColumns.DATE_EXPIRES); 1323 values.remove(MediaColumns.DISPLAY_NAME); 1324 values.remove(MediaColumns.BUCKET_ID); 1325 values.remove(MediaColumns.BUCKET_DISPLAY_NAME); 1326 1327 final String data = values.getAsString(MediaColumns.DATA); 1328 if (TextUtils.isEmpty(data)) return; 1329 1330 final File file = new File(data); 1331 final File fileLower = new File(data.toLowerCase(Locale.ROOT)); 1332 1333 values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data)); 1334 values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data)); 1335 final String displayName = extractDisplayName(data); 1336 final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName); 1337 if (matcher.matches()) { 1338 values.put(MediaColumns.IS_PENDING, 1339 matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0); 1340 values.put(MediaColumns.IS_TRASHED, 1341 matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0); 1342 values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2))); 1343 values.put(MediaColumns.DISPLAY_NAME, matcher.group(3)); 1344 } else { 1345 if (isForFuse) { 1346 // Allow Fuse thread to set IS_PENDING when using DATA column. 1347 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify 1348 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't 1349 // explicitly specify the value of IS_PENDING. 1350 } else { 1351 values.put(MediaColumns.IS_PENDING, 0); 1352 } 1353 values.put(MediaColumns.IS_TRASHED, 0); 1354 values.putNull(MediaColumns.DATE_EXPIRES); 1355 values.put(MediaColumns.DISPLAY_NAME, displayName); 1356 } 1357 1358 // Buckets are the parent directory 1359 final String parent = fileLower.getParent(); 1360 if (parent != null) { 1361 values.put(MediaColumns.BUCKET_ID, parent.hashCode()); 1362 // The relative path for files in the top directory is "/" 1363 if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) { 1364 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName()); 1365 } else { 1366 values.putNull(MediaColumns.BUCKET_DISPLAY_NAME); 1367 } 1368 } 1369 } 1370 1371 /** 1372 * Compute {@link MediaColumns#DATA} from several scattered 1373 * {@link MediaColumns} values. This method performs no enforcement of 1374 * argument validity. 1375 */ 1376 public static void computeDataFromValues(@NonNull ContentValues values, 1377 @NonNull File volumePath, boolean isForFuse) { 1378 values.remove(MediaColumns.DATA); 1379 1380 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 1381 final String resolvedDisplayName; 1382 // Pending file path shouldn't be rewritten for files inserted via filepath. 1383 if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) { 1384 final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES, 1385 (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000); 1386 final String combinedString = String.format( 1387 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName); 1388 // trim the file name to avoid ENAMETOOLONG error 1389 // after trim the file, if the user unpending the file, 1390 // the file name is not the original one 1391 resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES); 1392 } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) { 1393 final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES, 1394 (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000); 1395 final String combinedString = String.format( 1396 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName); 1397 // trim the file name to avoid ENAMETOOLONG error 1398 // after trim the file, if the user untrashes the file, 1399 // the file name is not the original one 1400 resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES); 1401 } else { 1402 resolvedDisplayName = displayName; 1403 } 1404 1405 String relativePath = values.getAsString(MediaColumns.RELATIVE_PATH); 1406 if (relativePath == null) { 1407 relativePath = ""; 1408 } 1409 try { 1410 final File filePath = buildPath(volumePath, relativePath, resolvedDisplayName); 1411 values.put(MediaColumns.DATA, filePath.getCanonicalPath()); 1412 } catch (IOException e) { 1413 throw new IllegalArgumentException( 1414 String.format("Failure in conversion to canonical file path. Failure path: %s.", 1415 relativePath.concat(resolvedDisplayName)), e); 1416 } 1417 } 1418 1419 public static void sanitizeValues(@NonNull ContentValues values, 1420 boolean rewriteHiddenFileName) { 1421 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); 1422 for (int i = 0; i < relativePath.length; i++) { 1423 relativePath[i] = sanitizeDisplayName(relativePath[i], rewriteHiddenFileName); 1424 } 1425 values.put(MediaColumns.RELATIVE_PATH, 1426 String.join("/", relativePath) + "/"); 1427 1428 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 1429 values.put(MediaColumns.DISPLAY_NAME, 1430 sanitizeDisplayName(displayName, rewriteHiddenFileName)); 1431 } 1432 1433 /** {@hide} **/ 1434 @Nullable 1435 public static String getAbsoluteSanitizedPath(String path) { 1436 final String[] pathSegments = sanitizePath(path); 1437 if (pathSegments.length == 0) { 1438 return null; 1439 } 1440 return path = "/" + String.join("/", 1441 Arrays.copyOfRange(pathSegments, 1, pathSegments.length)); 1442 } 1443 1444 /** {@hide} */ 1445 public static @NonNull String[] sanitizePath(@Nullable String path) { 1446 if (path == null) { 1447 return new String[0]; 1448 } else { 1449 final String[] segments = path.split("/"); 1450 // If the path corresponds to the top level directory, then we return an empty path 1451 // which denotes the top level directory 1452 if (segments.length == 0) { 1453 return new String[] { "" }; 1454 } 1455 for (int i = 0; i < segments.length; i++) { 1456 segments[i] = sanitizeDisplayName(segments[i]); 1457 } 1458 return segments; 1459 } 1460 } 1461 1462 /** 1463 * Sanitizes given name by mutating the file name to make it valid for a FAT filesystem. 1464 * @hide 1465 */ 1466 public static @Nullable String sanitizeDisplayName(@Nullable String name) { 1467 return sanitizeDisplayName(name, /*rewriteHiddenFileName*/ false); 1468 } 1469 1470 /** 1471 * Sanitizes given name by appending '_' to make it non-hidden and mutating the file name to 1472 * make it valid for a FAT filesystem. 1473 * @hide 1474 */ 1475 public static @Nullable String sanitizeDisplayName(@Nullable String name, 1476 boolean rewriteHiddenFileName) { 1477 if (name == null) { 1478 return null; 1479 } else if (rewriteHiddenFileName && name.startsWith(".")) { 1480 // The resulting file must not be hidden. 1481 return "_" + name; 1482 } else { 1483 return buildValidFatFilename(name); 1484 } 1485 } 1486 1487 /** 1488 * Returns true if the given File should be hidden (if it or any of its parents is hidden). 1489 * This can be called before the file is created, to check if it will be hidden once created. 1490 */ 1491 @VisibleForTesting 1492 public static boolean shouldFileBeHidden(@NonNull File file) { 1493 if (isFileHidden(file)) { 1494 return true; 1495 } 1496 1497 File parent = file.getParentFile(); 1498 while (parent != null) { 1499 if (isDirectoryHidden(parent)) { 1500 return true; 1501 } 1502 parent = parent.getParentFile(); 1503 } 1504 1505 return false; 1506 } 1507 1508 /** 1509 * Returns true if the given dir should be hidden (if it or any of its parents is hidden). 1510 * This can be called before the file is created, to check if it will be hidden once created. 1511 */ 1512 @VisibleForTesting 1513 public static boolean shouldDirBeHidden(@NonNull File file) { 1514 if (isDirectoryHidden(file)) { 1515 return true; 1516 } 1517 1518 File parent = file.getParentFile(); 1519 while (parent != null) { 1520 if (isDirectoryHidden(parent)) { 1521 return true; 1522 } 1523 parent = parent.getParentFile(); 1524 } 1525 1526 return false; 1527 } 1528 1529 /** 1530 * Test if this given directory should be considered hidden. 1531 */ 1532 @VisibleForTesting 1533 public static boolean isDirectoryHidden(@NonNull File dir) { 1534 final String name = dir.getName(); 1535 if (name.startsWith(".")) { 1536 return true; 1537 } 1538 1539 final File nomedia = new File(dir, ".nomedia"); 1540 1541 // check for .nomedia presence 1542 if (!nomedia.exists()) { 1543 return false; 1544 } 1545 1546 if (shouldBeVisible(dir.getAbsolutePath())) { 1547 nomedia.delete(); 1548 return false; 1549 } 1550 1551 // Handle top-level default directories. These directories should always be visible, 1552 // regardless of .nomedia presence. 1553 final String[] relativePath = sanitizePath(extractRelativePath(dir.getAbsolutePath())); 1554 final boolean isTopLevelDir = 1555 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); 1556 if (isTopLevelDir && isDefaultDirectoryName(name)) { 1557 nomedia.delete(); 1558 return false; 1559 } 1560 1561 // DCIM/Camera should always be visible regardless of .nomedia presence. 1562 if (CAMERA_RELATIVE_PATH.equalsIgnoreCase( 1563 extractRelativePathWithDisplayName(dir.getAbsolutePath()))) { 1564 nomedia.delete(); 1565 return false; 1566 } 1567 1568 if (isScreenshotsDirNonHidden(relativePath, name)) { 1569 nomedia.delete(); 1570 return false; 1571 } 1572 1573 // .nomedia is present which makes this directory as hidden directory 1574 Logging.logPersistent("Observed non-standard " + nomedia); 1575 return true; 1576 } 1577 1578 /** 1579 * Consider Screenshots directory in root directory or inside well-known directory as always 1580 * non-hidden. Nomedia file in these directories will not be able to hide these directories. 1581 * i.e., some examples of directories that will be considered non-hidden are 1582 * <ul> 1583 * <li> /storage/emulated/0/Screenshots or 1584 * <li> /storage/emulated/0/DCIM/Screenshots or 1585 * <li> /storage/emulated/0/Pictures/Screenshots ... 1586 * </ul> 1587 * Some examples of directories that can be considered as hidden with nomedia are 1588 * <ul> 1589 * <li> /storage/emulated/0/foo/Screenshots or 1590 * <li> /storage/emulated/0/DCIM/Foo/Screenshots or 1591 * <li> /storage/emulated/0/Pictures/foo/bar/Screenshots ... 1592 * </ul> 1593 */ 1594 private static boolean isScreenshotsDirNonHidden(@NonNull String[] relativePath, 1595 @NonNull String name) { 1596 if (name.equalsIgnoreCase(Environment.DIRECTORY_SCREENSHOTS)) { 1597 return (relativePath.length == 1 && 1598 (TextUtils.isEmpty(relativePath[0]) || isDefaultDirectoryName(relativePath[0]))); 1599 } 1600 return false; 1601 } 1602 1603 /** 1604 * Test if this given file should be considered hidden. 1605 */ 1606 @VisibleForTesting 1607 public static boolean isFileHidden(@NonNull File file) { 1608 final String name = file.getName(); 1609 1610 // Handle well-known file names that are pending or trashed; they 1611 // normally appear hidden, but we give them special treatment 1612 if (PATTERN_EXPIRES_FILE.matcher(name).matches()) { 1613 return false; 1614 } 1615 1616 // Otherwise fall back to file name 1617 if (name.startsWith(".")) { 1618 return true; 1619 } 1620 return false; 1621 } 1622 1623 /** 1624 * Clears all app's external cache directories, i.e. for each app we delete 1625 * /sdcard/Android/data/app/cache/* but we keep the directory itself. 1626 * 1627 * @return 0 in case of success, or {@link OsConstants#EIO} if any error occurs. 1628 * 1629 * <p>This method doesn't perform any checks, so make sure that the calling package is allowed 1630 * to clear cache directories first. 1631 * 1632 * <p>If this method returned {@link OsConstants#EIO}, then we can't guarantee whether all, none 1633 * or part of the directories were cleared. 1634 */ 1635 public static int clearAppCacheDirectories() { 1636 int status = 0; 1637 Log.i(TAG, "Clearing cache for all apps"); 1638 final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(), 1639 "Android", "data"); 1640 for (File appDataDir : rootDataDir.listFiles()) { 1641 try { 1642 final File appCacheDir = new File(appDataDir, "cache"); 1643 if (appCacheDir.isDirectory()) { 1644 FileUtils.deleteContents(appCacheDir); 1645 } 1646 } catch (Exception e) { 1647 // We want to avoid crashing MediaProvider at all costs, so we handle all "generic" 1648 // exceptions here, and just report to the caller that an IO exception has occurred. 1649 // We still try to clear the rest of the directories. 1650 Log.e(TAG, "Couldn't delete all app cache dirs!", e); 1651 status = OsConstants.EIO; 1652 } 1653 } 1654 return status; 1655 } 1656 1657 /** 1658 * @return {@code true} if {@code dir} has nomedia and it is dirty directory, so it should be 1659 * scanned. Returns {@code false} otherwise. 1660 */ 1661 public static boolean isDirectoryDirty(File dir) { 1662 File nomedia = new File(dir, ".nomedia"); 1663 1664 // We return false for directories that don't have .nomedia 1665 if (!nomedia.exists()) { 1666 return false; 1667 } 1668 1669 // We don't write to ".nomedia" dirs, only to ".nomedia" files. If this ".nomedia" is not 1670 // a file, then don't try to read it. 1671 if (!nomedia.isFile()) { 1672 return true; 1673 } 1674 1675 try { 1676 Optional<String> expectedPath = readString(nomedia); 1677 // Returns true If .nomedia file is empty or content doesn't match |dir| 1678 // Returns false otherwise 1679 return !expectedPath.isPresent() 1680 || !expectedPath.get().equals(dir.getPath()); 1681 } catch (IOException e) { 1682 Log.w(TAG, "Failed to read directory dirty" + dir); 1683 return true; 1684 } 1685 } 1686 1687 /** 1688 * {@code isDirty} == {@code true} will force {@code dir} scanning even if it's hidden 1689 * {@code isDirty} == {@code false} will skip {@code dir} scanning on next scan. 1690 */ 1691 public static void setDirectoryDirty(File dir, boolean isDirty) { 1692 File nomedia = new File(dir, ".nomedia"); 1693 if (nomedia.exists() && nomedia.isFile()) { 1694 try { 1695 writeString(nomedia, isDirty ? Optional.of("") : Optional.of(dir.getPath())); 1696 } catch (IOException e) { 1697 Log.w(TAG, "Failed to change directory dirty: " + dir + ". isDirty: " + isDirty); 1698 } 1699 } 1700 } 1701 1702 /** 1703 * @return the folder containing the top-most .nomedia in {@code file} hierarchy. 1704 * E.g input as /sdcard/foo/bar/ will return /sdcard/foo 1705 * even if foo and bar contain .nomedia files. 1706 * 1707 * Returns {@code null} if there's no .nomedia in hierarchy 1708 */ 1709 public static File getTopLevelNoMedia(@NonNull File file) { 1710 File topNoMediaDir = null; 1711 1712 File parent = file; 1713 while (parent != null) { 1714 File nomedia = new File(parent, ".nomedia"); 1715 if (nomedia.exists()) { 1716 topNoMediaDir = parent; 1717 } 1718 parent = parent.getParentFile(); 1719 } 1720 1721 return topNoMediaDir; 1722 } 1723 1724 /** 1725 * Generate the extended absolute path from the expired file path 1726 * E.g. the input expiredFilePath is /storage/emulated/0/DCIM/.trashed-1621147340-test.jpg 1727 * The returned result is /storage/emulated/0/DCIM/.trashed-1888888888-test.jpg 1728 * 1729 * @hide 1730 */ 1731 @Nullable 1732 public static String getAbsoluteExtendedPath(@NonNull String expiredFilePath, 1733 long extendedTime) { 1734 final String displayName = extractDisplayName(expiredFilePath); 1735 1736 final Matcher matcher = PATTERN_EXPIRES_FILE.matcher(displayName); 1737 if (matcher.matches()) { 1738 final String newDisplayName = String.format(Locale.US, ".%s-%d-%s", matcher.group(1), 1739 extendedTime, matcher.group(3)); 1740 final int lastSlash = expiredFilePath.lastIndexOf('/'); 1741 final String newPath = expiredFilePath.substring(0, lastSlash + 1).concat( 1742 newDisplayName); 1743 return newPath; 1744 } 1745 1746 return null; 1747 } 1748 1749 public static File buildPrimaryVolumeFile(int userId, String... segments) { 1750 return buildPath(new File("/storage/emulated/" + userId), segments); 1751 } 1752 1753 private static final String LOWER_FS_PREFIX = "/storage/"; 1754 private static final String FUSE_FS_PREFIX = "/mnt/user/" + UserHandle.myUserId() + "/"; 1755 1756 public static File toFuseFile(File file) { 1757 return new File(file.getPath().replaceFirst(LOWER_FS_PREFIX, FUSE_FS_PREFIX)); 1758 } 1759 1760 public static File fromFuseFile(File file) { 1761 return new File(file.getPath().replaceFirst(FUSE_FS_PREFIX, LOWER_FS_PREFIX)); 1762 } 1763 1764 @Nullable 1765 private static String getCanonicalPath(@Nullable String path) { 1766 if (path == null) return null; 1767 1768 try { 1769 return new File(path).getCanonicalPath(); 1770 } catch (IOException e) { 1771 Log.d(TAG, "Unable to get canonical path from invalid data path: " + path, e); 1772 return null; 1773 } 1774 } 1775 } 1776