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 + "(?:\\.picker_transcoded$)|" 1008 + "(?:(?:Movies|Music|Pictures)/.thumbnails$))"); 1009 1010 /** 1011 * The recordings directory. This is used for R OS. For S OS or later, 1012 * we use {@link Environment#DIRECTORY_RECORDINGS} directly. 1013 */ 1014 public static final String DIRECTORY_RECORDINGS = "Recordings"; 1015 1016 @VisibleForTesting 1017 public static final String[] DEFAULT_FOLDER_NAMES; 1018 static { 1019 if (SdkLevel.isAtLeastS()) { 1020 DEFAULT_FOLDER_NAMES = new String[]{ 1021 Environment.DIRECTORY_MUSIC, 1022 Environment.DIRECTORY_PODCASTS, 1023 Environment.DIRECTORY_RINGTONES, 1024 Environment.DIRECTORY_ALARMS, 1025 Environment.DIRECTORY_NOTIFICATIONS, 1026 Environment.DIRECTORY_PICTURES, 1027 Environment.DIRECTORY_MOVIES, 1028 Environment.DIRECTORY_DOWNLOADS, 1029 Environment.DIRECTORY_DCIM, 1030 Environment.DIRECTORY_DOCUMENTS, 1031 Environment.DIRECTORY_AUDIOBOOKS, 1032 Environment.DIRECTORY_RECORDINGS, 1033 }; 1034 } else { 1035 DEFAULT_FOLDER_NAMES = new String[]{ 1036 Environment.DIRECTORY_MUSIC, 1037 Environment.DIRECTORY_PODCASTS, 1038 Environment.DIRECTORY_RINGTONES, 1039 Environment.DIRECTORY_ALARMS, 1040 Environment.DIRECTORY_NOTIFICATIONS, 1041 Environment.DIRECTORY_PICTURES, 1042 Environment.DIRECTORY_MOVIES, 1043 Environment.DIRECTORY_DOWNLOADS, 1044 Environment.DIRECTORY_DCIM, 1045 Environment.DIRECTORY_DOCUMENTS, 1046 Environment.DIRECTORY_AUDIOBOOKS, 1047 DIRECTORY_RECORDINGS, 1048 }; 1049 } 1050 } 1051 1052 /** 1053 * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH} 1054 */ 1055 private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile( 1056 "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)"); 1057 1058 /** 1059 * Regex that matches paths under well-known storage paths. 1060 */ 1061 private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile( 1062 "(?i)^/storage/([^/]+)"); 1063 1064 /** 1065 * Regex that matches user-ids under well-known storage paths. 1066 */ 1067 private static final Pattern PATTERN_USER_ID = Pattern.compile( 1068 "(?i)^/storage/emulated/([0-9]+)"); 1069 1070 private static final String CAMERA_RELATIVE_PATH = 1071 String.format("%s/%s/", Environment.DIRECTORY_DCIM, "Camera"); 1072 1073 public static boolean isCrossUserEnabled() { 1074 return PROP_CROSS_USER_ALLOWED || SdkLevel.isAtLeastS(); 1075 } 1076 1077 private static @Nullable String normalizeUuid(@Nullable String fsUuid) { 1078 return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null; 1079 } 1080 1081 public static int extractUserId(@Nullable String data) { 1082 if (data == null) return -1; 1083 final Matcher matcher = PATTERN_USER_ID.matcher(data); 1084 if (matcher.find()) { 1085 return Integer.parseInt(matcher.group(1)); 1086 } 1087 1088 return -1; 1089 } 1090 1091 public static @Nullable String extractVolumePath(@Nullable String data) { 1092 if (data == null) return null; 1093 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 1094 if (matcher.find()) { 1095 return data.substring(0, matcher.end()); 1096 } else { 1097 return null; 1098 } 1099 } 1100 1101 public static @Nullable String extractVolumeName(@Nullable String data) { 1102 if (data == null) return null; 1103 final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data); 1104 if (matcher.find()) { 1105 final String volumeName = matcher.group(1); 1106 if (volumeName.equals("emulated")) { 1107 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 1108 } else { 1109 return normalizeUuid(volumeName); 1110 } 1111 } else { 1112 return MediaStore.VOLUME_INTERNAL; 1113 } 1114 } 1115 1116 public static @Nullable String extractRelativePath(@Nullable String data) { 1117 if (data == null) return null; 1118 1119 final String path; 1120 try { 1121 path = getCanonicalPath(data); 1122 } catch (IOException e) { 1123 Log.d(TAG, "Unable to get canonical path from invalid data path: " + data, e); 1124 return null; 1125 } 1126 1127 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path); 1128 if (matcher.find()) { 1129 final int lastSlash = path.lastIndexOf('/'); 1130 if (lastSlash == -1 || lastSlash < matcher.end()) { 1131 // This is a file in the top-level directory, so relative path is "/" 1132 // which is different than null, which means unknown path 1133 return "/"; 1134 } else { 1135 return path.substring(matcher.end(), lastSlash + 1); 1136 } 1137 } else { 1138 return null; 1139 } 1140 } 1141 1142 /** 1143 * Returns relative path with display name. 1144 */ 1145 @VisibleForTesting 1146 public static @Nullable String extractRelativePathWithDisplayName(@Nullable String path) { 1147 if (path == null) return null; 1148 1149 if (path.equals("/storage/emulated") || path.equals("/storage/emulated/")) { 1150 // This path is not reachable for MediaProvider. 1151 return null; 1152 } 1153 1154 // We are extracting relative path for the directory itself, we add "/" so that we can use 1155 // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative 1156 // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for 1157 // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/". 1158 if (!path.endsWith("/")) { 1159 // Relative path for directory should end with "/". 1160 path += "/"; 1161 } 1162 1163 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path); 1164 if (matcher.find()) { 1165 if (matcher.end() == path.length()) { 1166 // This is the top-level directory, so relative path is "/" 1167 return "/"; 1168 } 1169 return path.substring(matcher.end()); 1170 } 1171 return null; 1172 } 1173 1174 public static @Nullable String extractPathOwnerPackageName(@Nullable String path) { 1175 if (path == null) return null; 1176 final Matcher m = PATTERN_OWNED_PATH.matcher(path); 1177 if (m.matches()) { 1178 return m.group(1); 1179 } 1180 return null; 1181 } 1182 1183 public static @Nullable String extractOwnerPackageNameFromRelativePath(@Nullable String path) { 1184 if (path == null) return null; 1185 final Matcher m = PATTERN_OWNED_RELATIVE_PATH.matcher(path); 1186 if (m.matches()) { 1187 return m.group(1); 1188 } 1189 return null; 1190 } 1191 1192 public static boolean isExternalMediaDirectory(@NonNull String path) { 1193 return isExternalMediaDirectory(path, PROP_CROSS_USER_ROOT); 1194 } 1195 1196 @VisibleForTesting 1197 static boolean isExternalMediaDirectory(@NonNull String path, String crossUserRoot) { 1198 final String relativePath = extractRelativePath(path); 1199 if (relativePath == null) { 1200 return false; 1201 } 1202 1203 if (StringUtils.startsWithIgnoreCase(relativePath, "Android/media")) { 1204 return true; 1205 } 1206 if (!TextUtils.isEmpty(crossUserRoot)) { 1207 return StringUtils.startsWithIgnoreCase(relativePath, crossUserRoot + "/Android/media"); 1208 } 1209 1210 return false; 1211 } 1212 1213 /** 1214 * Returns true if path is Android/data or Android/obb path. 1215 */ 1216 public static boolean isDataOrObbPath(@Nullable String path) { 1217 if (path == null) return false; 1218 final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path); 1219 return m.matches(); 1220 } 1221 1222 /** 1223 * Returns true if relative path is Android/data or Android/obb path. 1224 */ 1225 public static boolean isDataOrObbRelativePath(@Nullable String path) { 1226 if (path == null) return false; 1227 final Matcher m = PATTERN_DATA_OR_OBB_RELATIVE_PATH.matcher(path); 1228 return m.matches(); 1229 } 1230 1231 /** 1232 * Returns true if relative path is Android/obb path. 1233 */ 1234 public static boolean isObbOrChildRelativePath(@Nullable String path) { 1235 if (path == null) return false; 1236 final Matcher m = PATTERN_OBB_OR_CHILD_RELATIVE_PATH.matcher(path); 1237 return m.matches(); 1238 } 1239 1240 public static boolean shouldBeVisible(@Nullable String path) { 1241 if (path == null) return false; 1242 final Matcher m = PATTERN_VISIBLE.matcher(path); 1243 return m.matches(); 1244 } 1245 1246 public static boolean shouldBeInvisible(@Nullable String path) { 1247 if (path == null) return false; 1248 final Matcher m = PATTERN_INVISIBLE.matcher(path); 1249 return m.matches(); 1250 } 1251 1252 /** 1253 * Returns the name of the top level directory, or null if the path doesn't go through the 1254 * external storage directory. 1255 */ 1256 @Nullable 1257 public static String extractTopLevelDir(String path) { 1258 final String relativePath = extractRelativePath(path); 1259 if (relativePath == null) { 1260 return null; 1261 } 1262 1263 return extractTopLevelDir(relativePath.split("/")); 1264 } 1265 1266 @Nullable 1267 public static String extractTopLevelDir(String[] relativePathSegments) { 1268 return extractTopLevelDir(relativePathSegments, PROP_CROSS_USER_ROOT); 1269 } 1270 1271 @VisibleForTesting 1272 @Nullable 1273 static String extractTopLevelDir(String[] relativePathSegments, String crossUserRoot) { 1274 if (relativePathSegments == null) return null; 1275 1276 final String topLevelDir = relativePathSegments.length > 0 ? relativePathSegments[0] : null; 1277 if (crossUserRoot != null && crossUserRoot.equals(topLevelDir)) { 1278 return relativePathSegments.length > 1 ? relativePathSegments[1] : null; 1279 } 1280 1281 return topLevelDir; 1282 } 1283 1284 public static boolean isDefaultDirectoryName(@Nullable String dirName) { 1285 for (String defaultDirName : DEFAULT_FOLDER_NAMES) { 1286 if (defaultDirName.equalsIgnoreCase(dirName)) { 1287 return true; 1288 } 1289 } 1290 return false; 1291 } 1292 1293 /** 1294 * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other 1295 * columns being modified by this operation. 1296 */ 1297 public static void computeDateExpires(@NonNull ContentValues values) { 1298 // External apps have no ability to change this field 1299 values.remove(MediaColumns.DATE_EXPIRES); 1300 1301 // Only define the field when this modification is actually adjusting 1302 // one of the flags that should influence the expiration 1303 final Object pending = values.get(MediaColumns.IS_PENDING); 1304 if (pending != null) { 1305 if (parseBoolean(pending, false)) { 1306 values.put(MediaColumns.DATE_EXPIRES, 1307 (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000); 1308 } else { 1309 values.putNull(MediaColumns.DATE_EXPIRES); 1310 } 1311 } 1312 final Object trashed = values.get(MediaColumns.IS_TRASHED); 1313 if (trashed != null) { 1314 if (parseBoolean(trashed, false)) { 1315 values.put(MediaColumns.DATE_EXPIRES, 1316 (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000); 1317 } else { 1318 values.putNull(MediaColumns.DATE_EXPIRES); 1319 } 1320 } 1321 } 1322 1323 /** 1324 * Compute several scattered {@link MediaColumns} values from 1325 * {@link MediaColumns#DATA}. This method performs no enforcement of 1326 * argument validity. 1327 */ 1328 public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) { 1329 // Worst case we have to assume no bucket details 1330 values.remove(MediaColumns.VOLUME_NAME); 1331 values.remove(MediaColumns.RELATIVE_PATH); 1332 values.remove(MediaColumns.IS_TRASHED); 1333 values.remove(MediaColumns.DATE_EXPIRES); 1334 values.remove(MediaColumns.DISPLAY_NAME); 1335 values.remove(MediaColumns.BUCKET_ID); 1336 values.remove(MediaColumns.BUCKET_DISPLAY_NAME); 1337 1338 String data = values.getAsString(MediaColumns.DATA); 1339 if (TextUtils.isEmpty(data)) return; 1340 1341 try { 1342 data = new File(data).getCanonicalPath(); 1343 values.put(MediaColumns.DATA, data); 1344 } catch (IOException e) { 1345 throw new IllegalArgumentException( 1346 String.format(Locale.ROOT, "Invalid file path:%s in request.", data)); 1347 } 1348 1349 final File file = new File(data); 1350 final File fileLower = new File(data.toLowerCase(Locale.ROOT)); 1351 1352 values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data)); 1353 values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data)); 1354 final String displayName = extractDisplayName(data); 1355 final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName); 1356 if (matcher.matches()) { 1357 values.put(MediaColumns.IS_PENDING, 1358 matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0); 1359 values.put(MediaColumns.IS_TRASHED, 1360 matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0); 1361 values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2))); 1362 values.put(MediaColumns.DISPLAY_NAME, matcher.group(3)); 1363 } else { 1364 if (isForFuse) { 1365 // Allow Fuse thread to set IS_PENDING when using DATA column. 1366 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify 1367 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't 1368 // explicitly specify the value of IS_PENDING. 1369 } else { 1370 values.put(MediaColumns.IS_PENDING, 0); 1371 } 1372 values.put(MediaColumns.IS_TRASHED, 0); 1373 values.putNull(MediaColumns.DATE_EXPIRES); 1374 values.put(MediaColumns.DISPLAY_NAME, displayName); 1375 } 1376 1377 // Buckets are the parent directory 1378 final String parent = fileLower.getParent(); 1379 if (parent != null) { 1380 values.put(MediaColumns.BUCKET_ID, parent.hashCode()); 1381 // The relative path for files in the top directory is "/" 1382 if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) { 1383 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName()); 1384 } else { 1385 values.putNull(MediaColumns.BUCKET_DISPLAY_NAME); 1386 } 1387 } 1388 } 1389 1390 /** 1391 * Compute {@link MediaColumns#DATA} from several scattered 1392 * {@link MediaColumns} values. This method performs no enforcement of 1393 * argument validity. 1394 */ 1395 public static void computeDataFromValues(@NonNull ContentValues values, 1396 @NonNull File volumePath, boolean isForFuse) { 1397 values.remove(MediaColumns.DATA); 1398 1399 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 1400 final String resolvedDisplayName; 1401 // Pending file path shouldn't be rewritten for files inserted via filepath. 1402 if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) { 1403 final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES, 1404 (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000); 1405 final String combinedString = String.format( 1406 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName); 1407 // trim the file name to avoid ENAMETOOLONG error 1408 // after trim the file, if the user unpending the file, 1409 // the file name is not the original one 1410 resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES); 1411 } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) { 1412 final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES, 1413 (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000); 1414 final String combinedString = String.format( 1415 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName); 1416 // trim the file name to avoid ENAMETOOLONG error 1417 // after trim the file, if the user untrashes the file, 1418 // the file name is not the original one 1419 resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES); 1420 } else { 1421 resolvedDisplayName = displayName; 1422 } 1423 1424 String relativePath = values.getAsString(MediaColumns.RELATIVE_PATH); 1425 if (relativePath == null) { 1426 relativePath = ""; 1427 } 1428 try { 1429 final File filePath = buildPath(volumePath, relativePath, resolvedDisplayName); 1430 values.put(MediaColumns.DATA, filePath.getCanonicalPath()); 1431 } catch (IOException e) { 1432 throw new IllegalArgumentException( 1433 String.format("Failure in conversion to canonical file path. Failure path: %s.", 1434 relativePath.concat(resolvedDisplayName)), e); 1435 } 1436 } 1437 1438 @VisibleForTesting 1439 static ArrayMap<String, String> sAudioTypes = new ArrayMap<>(); 1440 1441 static { 1442 sAudioTypes.put(Environment.DIRECTORY_RINGTONES, AudioColumns.IS_RINGTONE); 1443 sAudioTypes.put(Environment.DIRECTORY_NOTIFICATIONS, AudioColumns.IS_NOTIFICATION); 1444 sAudioTypes.put(Environment.DIRECTORY_ALARMS, AudioColumns.IS_ALARM); 1445 sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST); 1446 sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK); 1447 sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC); 1448 if (SdkLevel.isAtLeastS()) { 1449 sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING); 1450 } else { 1451 sAudioTypes.put(FileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING); 1452 } 1453 } 1454 1455 /** 1456 * Compute values for columns in {@code sAudioTypes} based on the given {@code filePath}. 1457 */ 1458 public static void computeAudioTypeValuesFromData(@NonNull String filePath, 1459 @NonNull ObjIntConsumer<String> consumer) { 1460 final String lowPath = filePath.toLowerCase(Locale.ROOT); 1461 boolean anyMatch = false; 1462 for (int i = 0; i < sAudioTypes.size(); i++) { 1463 final boolean match = lowPath 1464 .contains('/' + sAudioTypes.keyAt(i).toLowerCase(Locale.ROOT) + '/'); 1465 consumer.accept(sAudioTypes.valueAt(i), match ? 1 : 0); 1466 anyMatch |= match; 1467 } 1468 if (!anyMatch) { 1469 consumer.accept(AudioColumns.IS_MUSIC, 1); 1470 } 1471 } 1472 1473 public static void sanitizeValues(@NonNull ContentValues values, 1474 boolean rewriteHiddenFileName) { 1475 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); 1476 for (int i = 0; i < relativePath.length; i++) { 1477 relativePath[i] = sanitizeDisplayName(relativePath[i], rewriteHiddenFileName); 1478 } 1479 values.put(MediaColumns.RELATIVE_PATH, 1480 String.join("/", relativePath) + "/"); 1481 1482 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 1483 values.put(MediaColumns.DISPLAY_NAME, 1484 sanitizeDisplayName(displayName, rewriteHiddenFileName)); 1485 } 1486 1487 /** {@hide} **/ 1488 @Nullable 1489 public static String getAbsoluteSanitizedPath(String path) { 1490 final String[] pathSegments = sanitizePath(path); 1491 if (pathSegments.length == 0) { 1492 return null; 1493 } 1494 return path = "/" + String.join("/", 1495 Arrays.copyOfRange(pathSegments, 1, pathSegments.length)); 1496 } 1497 1498 /** {@hide} */ 1499 public static @NonNull String[] sanitizePath(@Nullable String path) { 1500 if (path == null) { 1501 return new String[0]; 1502 } else { 1503 final String[] segments = path.split("/"); 1504 // If the path corresponds to the top level directory, then we return an empty path 1505 // which denotes the top level directory 1506 if (segments.length == 0) { 1507 return new String[] { "" }; 1508 } 1509 for (int i = 0; i < segments.length; i++) { 1510 segments[i] = sanitizeDisplayName(segments[i]); 1511 } 1512 return segments; 1513 } 1514 } 1515 1516 /** 1517 * Sanitizes given name by mutating the file name to make it valid for a FAT filesystem. 1518 * @hide 1519 */ 1520 public static @Nullable String sanitizeDisplayName(@Nullable String name) { 1521 return sanitizeDisplayName(name, /*rewriteHiddenFileName*/ false); 1522 } 1523 1524 /** 1525 * Sanitizes given name by appending '_' to make it non-hidden and mutating the file name to 1526 * make it valid for a FAT filesystem. 1527 * @hide 1528 */ 1529 public static @Nullable String sanitizeDisplayName(@Nullable String name, 1530 boolean rewriteHiddenFileName) { 1531 if (name == null) { 1532 return null; 1533 } else if (rewriteHiddenFileName && name.startsWith(".")) { 1534 // The resulting file must not be hidden. 1535 return "_" + name; 1536 } else { 1537 return buildValidFatFilename(name); 1538 } 1539 } 1540 1541 /** 1542 * Returns true if the given File should be hidden (if it or any of its parents is hidden). 1543 * This can be called before the file is created, to check if it will be hidden once created. 1544 */ 1545 @VisibleForTesting 1546 public static boolean shouldFileBeHidden(@NonNull File file) { 1547 if (isFileHidden(file)) { 1548 return true; 1549 } 1550 1551 File parent = file.getParentFile(); 1552 while (parent != null) { 1553 if (isDirectoryHidden(parent)) { 1554 return true; 1555 } 1556 parent = parent.getParentFile(); 1557 } 1558 1559 return false; 1560 } 1561 1562 /** 1563 * Returns true if the given dir should be hidden (if it or any of its parents is hidden). 1564 * This can be called before the file is created, to check if it will be hidden once created. 1565 */ 1566 @VisibleForTesting 1567 public static boolean shouldDirBeHidden(@NonNull File file) { 1568 if (isDirectoryHidden(file)) { 1569 return true; 1570 } 1571 1572 File parent = file.getParentFile(); 1573 while (parent != null) { 1574 if (isDirectoryHidden(parent)) { 1575 return true; 1576 } 1577 parent = parent.getParentFile(); 1578 } 1579 1580 return false; 1581 } 1582 1583 /** 1584 * Test if this given directory should be considered hidden. 1585 */ 1586 @VisibleForTesting 1587 public static boolean isDirectoryHidden(@NonNull File dir) { 1588 final String name = dir.getName(); 1589 if (name.startsWith(".")) { 1590 return true; 1591 } 1592 1593 final File nomedia = new File(dir, ".nomedia"); 1594 1595 // check for .nomedia presence 1596 if (!nomedia.exists()) { 1597 return false; 1598 } 1599 1600 if (shouldBeVisible(dir.getAbsolutePath())) { 1601 nomedia.delete(); 1602 return false; 1603 } 1604 1605 // Handle top-level default directories. These directories should always be visible, 1606 // regardless of .nomedia presence. 1607 final String[] relativePath = sanitizePath(extractRelativePath(dir.getAbsolutePath())); 1608 final boolean isTopLevelDir = 1609 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); 1610 if (isTopLevelDir && isDefaultDirectoryName(name)) { 1611 nomedia.delete(); 1612 return false; 1613 } 1614 1615 // DCIM/Camera should always be visible regardless of .nomedia presence. 1616 if (CAMERA_RELATIVE_PATH.equalsIgnoreCase( 1617 extractRelativePathWithDisplayName(dir.getAbsolutePath()))) { 1618 nomedia.delete(); 1619 return false; 1620 } 1621 1622 if (isScreenshotsDirNonHidden(relativePath, name)) { 1623 nomedia.delete(); 1624 return false; 1625 } 1626 1627 // .nomedia is present which makes this directory as hidden directory 1628 Logging.logPersistent("Observed non-standard " + nomedia); 1629 return true; 1630 } 1631 1632 /** 1633 * Consider Screenshots directory in root directory or inside well-known directory as always 1634 * non-hidden. Nomedia file in these directories will not be able to hide these directories. 1635 * i.e., some examples of directories that will be considered non-hidden are 1636 * <ul> 1637 * <li> /storage/emulated/0/Screenshots or 1638 * <li> /storage/emulated/0/DCIM/Screenshots or 1639 * <li> /storage/emulated/0/Pictures/Screenshots ... 1640 * </ul> 1641 * Some examples of directories that can be considered as hidden with nomedia are 1642 * <ul> 1643 * <li> /storage/emulated/0/foo/Screenshots or 1644 * <li> /storage/emulated/0/DCIM/Foo/Screenshots or 1645 * <li> /storage/emulated/0/Pictures/foo/bar/Screenshots ... 1646 * </ul> 1647 */ 1648 private static boolean isScreenshotsDirNonHidden(@NonNull String[] relativePath, 1649 @NonNull String name) { 1650 if (name.equalsIgnoreCase(Environment.DIRECTORY_SCREENSHOTS)) { 1651 return (relativePath.length == 1 && 1652 (TextUtils.isEmpty(relativePath[0]) || isDefaultDirectoryName(relativePath[0]))); 1653 } 1654 return false; 1655 } 1656 1657 /** 1658 * Test if this given file should be considered hidden. 1659 */ 1660 @VisibleForTesting 1661 public static boolean isFileHidden(@NonNull File file) { 1662 final String name = file.getName(); 1663 1664 // Handle well-known file names that are pending or trashed; they 1665 // normally appear hidden, but we give them special treatment 1666 if (PATTERN_EXPIRES_FILE.matcher(name).matches()) { 1667 return false; 1668 } 1669 1670 // Otherwise fall back to file name 1671 if (name.startsWith(".")) { 1672 return true; 1673 } 1674 return false; 1675 } 1676 1677 /** 1678 * Clears all app's external cache directories, i.e. for each app we delete 1679 * /sdcard/Android/data/app/cache/* but we keep the directory itself. 1680 * 1681 * @return 0 in case of success, or {@link OsConstants#EIO} if any error occurs. 1682 * 1683 * <p>This method doesn't perform any checks, so make sure that the calling package is allowed 1684 * to clear cache directories first. 1685 * 1686 * <p>If this method returned {@link OsConstants#EIO}, then we can't guarantee whether all, none 1687 * or part of the directories were cleared. 1688 */ 1689 public static int clearAppCacheDirectories() { 1690 int status = 0; 1691 Log.i(TAG, "Clearing cache for all apps"); 1692 final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(), 1693 "Android", "data"); 1694 File[] appDataDirs = rootDataDir.listFiles(); 1695 if (appDataDirs == null) { 1696 // Couldn't delete any app cache dirs because the call to list files in root data dir 1697 // failed (b/234521806). It is not clear why this call would fail because root data 1698 // dir path should be well-formed. 1699 Log.e(TAG, String.format("Couldn't delete any app cache dirs in root data dir %s !", 1700 rootDataDir.getAbsolutePath())); 1701 status = OsConstants.EIO; 1702 } else { 1703 for (File appDataDir : appDataDirs) { 1704 try { 1705 final File appCacheDir = new File(appDataDir, "cache"); 1706 if (appCacheDir.isDirectory()) { 1707 FileUtils.deleteContents(appCacheDir); 1708 } 1709 } catch (Exception e) { 1710 // We want to avoid crashing MediaProvider at all costs, so we handle all 1711 // "generic" exceptions here, and just report to the caller that an IO exception 1712 // has occurred. We still try to clear the rest of the directories. 1713 Log.e(TAG, "Couldn't delete all app cache dirs!", e); 1714 status = OsConstants.EIO; 1715 } 1716 } 1717 } 1718 return status; 1719 } 1720 1721 /** 1722 * @return {@code true} if {@code dir} has nomedia and it is dirty directory, so it should be 1723 * scanned. Returns {@code false} otherwise. 1724 */ 1725 public static boolean isDirectoryDirty(File dir) { 1726 File nomedia = new File(dir, ".nomedia"); 1727 1728 // We return false for directories that don't have .nomedia 1729 if (!nomedia.exists()) { 1730 return false; 1731 } 1732 1733 // We don't write to ".nomedia" dirs, only to ".nomedia" files. If this ".nomedia" is not 1734 // a file, then don't try to read it. 1735 if (!nomedia.isFile()) { 1736 return true; 1737 } 1738 1739 try { 1740 Optional<String> expectedPath = readString(nomedia); 1741 // Returns true If .nomedia file is empty or content doesn't match |dir| 1742 // Returns false otherwise 1743 return !expectedPath.isPresent() 1744 || !expectedPath.get().equalsIgnoreCase(dir.getPath()); 1745 } catch (IOException e) { 1746 Log.w(TAG, "Failed to read directory dirty" + dir); 1747 return true; 1748 } 1749 } 1750 1751 /** 1752 * {@code isDirty} == {@code true} will force {@code dir} scanning even if it's hidden 1753 * {@code isDirty} == {@code false} will skip {@code dir} scanning on next scan. 1754 */ 1755 public static void setDirectoryDirty(File dir, boolean isDirty) { 1756 File nomedia = new File(dir, ".nomedia"); 1757 if (nomedia.exists() && nomedia.isFile()) { 1758 try { 1759 writeString(nomedia, isDirty ? Optional.of("") : Optional.of(dir.getPath())); 1760 } catch (IOException e) { 1761 Log.w(TAG, "Failed to change directory dirty: " + dir + ". isDirty: " + isDirty); 1762 } 1763 } 1764 } 1765 1766 /** 1767 * @return the folder containing the top-most .nomedia in {@code file} hierarchy. 1768 * E.g input as /sdcard/foo/bar/ will return /sdcard/foo 1769 * even if foo and bar contain .nomedia files. 1770 * 1771 * Returns {@code null} if there's no .nomedia in hierarchy 1772 */ 1773 public static File getTopLevelNoMedia(@NonNull File file) { 1774 File topNoMediaDir = null; 1775 1776 File parent = file; 1777 while (parent != null) { 1778 File nomedia = new File(parent, ".nomedia"); 1779 if (nomedia.exists()) { 1780 topNoMediaDir = parent; 1781 } 1782 parent = parent.getParentFile(); 1783 } 1784 1785 return topNoMediaDir; 1786 } 1787 1788 /** 1789 * Generate the extended absolute path from the expired file path 1790 * E.g. the input expiredFilePath is /storage/emulated/0/DCIM/.trashed-1621147340-test.jpg 1791 * The returned result is /storage/emulated/0/DCIM/.trashed-1888888888-test.jpg 1792 * 1793 * @hide 1794 */ 1795 @Nullable 1796 public static String getAbsoluteExtendedPath(@NonNull String expiredFilePath, 1797 long extendedTime) { 1798 final String displayName = extractDisplayName(expiredFilePath); 1799 1800 final Matcher matcher = PATTERN_EXPIRES_FILE.matcher(displayName); 1801 if (matcher.matches()) { 1802 final String newDisplayName = String.format(Locale.US, ".%s-%d-%s", matcher.group(1), 1803 extendedTime, matcher.group(3)); 1804 final int lastSlash = expiredFilePath.lastIndexOf('/'); 1805 final String newPath = expiredFilePath.substring(0, lastSlash + 1).concat( 1806 newDisplayName); 1807 return newPath; 1808 } 1809 1810 return null; 1811 } 1812 1813 public static File buildPrimaryVolumeFile(int userId, String... segments) { 1814 return buildPath(new File("/storage/emulated/" + userId), segments); 1815 } 1816 1817 private static final String LOWER_FS_PREFIX = "/storage/"; 1818 private static final String FUSE_FS_PREFIX = "/mnt/user/" + UserHandle.myUserId() + "/"; 1819 1820 public static File toFuseFile(File file) { 1821 return new File(file.getPath().replaceFirst(LOWER_FS_PREFIX, FUSE_FS_PREFIX)); 1822 } 1823 1824 public static File fromFuseFile(File file) { 1825 return new File(file.getPath().replaceFirst(FUSE_FS_PREFIX, LOWER_FS_PREFIX)); 1826 } 1827 1828 /** 1829 * Returns the canonical {@link File} for the provided abstract pathname. 1830 * 1831 * @return The canonical pathname string denoting the same file or directory as this abstract 1832 * pathname 1833 * @see File#getCanonicalFile() 1834 */ 1835 @NonNull 1836 public static File getCanonicalFile(@NonNull String path) throws IOException { 1837 Objects.requireNonNull(path); 1838 return new File(path).getCanonicalFile(); 1839 } 1840 1841 /** 1842 * Returns the canonical pathname string of the provided abstract pathname. 1843 * 1844 * @return The canonical pathname string denoting the same file or directory as this abstract 1845 * pathname. 1846 * @see File#getCanonicalPath() 1847 */ 1848 @NonNull 1849 public static String getCanonicalPath(@NonNull String path) throws IOException { 1850 Objects.requireNonNull(path); 1851 return new File(path).getCanonicalPath(); 1852 } 1853 1854 /** 1855 * A wrapper for {@link File#getCanonicalFile()} that catches {@link IOException}-s and 1856 * re-throws them as {@link RuntimeException}-s. 1857 * 1858 * @see File#getCanonicalFile() 1859 */ 1860 @NonNull 1861 public static File canonicalize(@NonNull File file) throws IOException { 1862 Objects.requireNonNull(file); 1863 return file.getCanonicalFile(); 1864 } 1865 } 1866