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