1 /* 2 * Copyright (C) 2017 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.internal.content; 18 19 import android.annotation.CallSuper; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.AssetFileDescriptor; 26 import android.database.Cursor; 27 import android.database.MatrixCursor; 28 import android.database.MatrixCursor.RowBuilder; 29 import android.graphics.Point; 30 import android.net.Uri; 31 import android.os.Binder; 32 import android.os.Bundle; 33 import android.os.CancellationSignal; 34 import android.os.FileObserver; 35 import android.os.FileUtils; 36 import android.os.Handler; 37 import android.os.ParcelFileDescriptor; 38 import android.provider.DocumentsContract; 39 import android.provider.DocumentsContract.Document; 40 import android.provider.DocumentsProvider; 41 import android.provider.MediaStore; 42 import android.provider.MetadataReader; 43 import android.system.Int64Ref; 44 import android.text.TextUtils; 45 import android.util.ArrayMap; 46 import android.util.Log; 47 import android.webkit.MimeTypeMap; 48 49 import com.android.internal.annotations.GuardedBy; 50 import com.android.internal.util.ArrayUtils; 51 52 import libcore.io.IoUtils; 53 54 import java.io.File; 55 import java.io.FileInputStream; 56 import java.io.FileNotFoundException; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.nio.file.FileSystems; 60 import java.nio.file.FileVisitResult; 61 import java.nio.file.FileVisitor; 62 import java.nio.file.Files; 63 import java.nio.file.Path; 64 import java.nio.file.attribute.BasicFileAttributes; 65 import java.util.Arrays; 66 import java.util.LinkedList; 67 import java.util.List; 68 import java.util.Locale; 69 import java.util.Set; 70 import java.util.concurrent.CopyOnWriteArrayList; 71 import java.util.function.Predicate; 72 import java.util.regex.Pattern; 73 74 /** 75 * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local 76 * files. 77 */ 78 public abstract class FileSystemProvider extends DocumentsProvider { 79 80 private static final String TAG = "FileSystemProvider"; 81 82 private static final boolean LOG_INOTIFY = false; 83 84 protected static final String SUPPORTED_QUERY_ARGS = joinNewline( 85 DocumentsContract.QUERY_ARG_DISPLAY_NAME, 86 DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, 87 DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, 88 DocumentsContract.QUERY_ARG_MIME_TYPES); 89 joinNewline(String... args)90 private static String joinNewline(String... args) { 91 return TextUtils.join("\n", args); 92 } 93 94 private String[] mDefaultProjection; 95 96 @GuardedBy("mObservers") 97 private final ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>(); 98 99 private Handler mHandler; 100 getFileForDocId(String docId, boolean visible)101 protected abstract File getFileForDocId(String docId, boolean visible) 102 throws FileNotFoundException; 103 getDocIdForFile(File file)104 protected abstract String getDocIdForFile(File file) throws FileNotFoundException; 105 buildNotificationUri(String docId)106 protected abstract Uri buildNotificationUri(String docId); 107 108 /** 109 * Callback indicating that the given document has been modified. This gives 110 * the provider a hook to invalidate cached data, such as {@code sdcardfs}. 111 */ onDocIdChanged(String docId)112 protected void onDocIdChanged(String docId) { 113 // Default is no-op 114 } 115 116 /** 117 * Callback indicating that the given document has been deleted or moved. This gives 118 * the provider a hook to revoke the uri permissions. 119 */ onDocIdDeleted(String docId)120 protected void onDocIdDeleted(String docId) { 121 // Default is no-op 122 } 123 124 @Override onCreate()125 public boolean onCreate() { 126 throw new UnsupportedOperationException( 127 "Subclass should override this and call onCreate(defaultDocumentProjection)"); 128 } 129 130 @CallSuper onCreate(String[] defaultProjection)131 protected void onCreate(String[] defaultProjection) { 132 mHandler = new Handler(); 133 mDefaultProjection = defaultProjection; 134 } 135 136 @Override isChildDocument(String parentDocId, String docId)137 public boolean isChildDocument(String parentDocId, String docId) { 138 try { 139 final File parent = getFileForDocId(parentDocId).getCanonicalFile(); 140 final File doc = getFileForDocId(docId).getCanonicalFile(); 141 return FileUtils.contains(parent, doc); 142 } catch (IOException e) { 143 throw new IllegalArgumentException( 144 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); 145 } 146 } 147 148 @Override getDocumentMetadata(String documentId)149 public @Nullable Bundle getDocumentMetadata(String documentId) 150 throws FileNotFoundException { 151 File file = getFileForDocId(documentId); 152 153 if (!file.exists()) { 154 throw new FileNotFoundException("Can't find the file for documentId: " + documentId); 155 } 156 157 final String mimeType = getDocumentType(documentId); 158 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 159 final Int64Ref treeCount = new Int64Ref(0); 160 final Int64Ref treeSize = new Int64Ref(0); 161 try { 162 final Path path = FileSystems.getDefault().getPath(file.getAbsolutePath()); 163 Files.walkFileTree(path, new FileVisitor<Path>() { 164 @Override 165 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { 166 return FileVisitResult.CONTINUE; 167 } 168 169 @Override 170 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 171 treeCount.value += 1; 172 treeSize.value += attrs.size(); 173 return FileVisitResult.CONTINUE; 174 } 175 176 @Override 177 public FileVisitResult visitFileFailed(Path file, IOException exc) { 178 return FileVisitResult.CONTINUE; 179 } 180 181 @Override 182 public FileVisitResult postVisitDirectory(Path dir, IOException exc) { 183 return FileVisitResult.CONTINUE; 184 } 185 }); 186 } catch (IOException e) { 187 Log.e(TAG, "An error occurred retrieving the metadata", e); 188 return null; 189 } 190 191 final Bundle res = new Bundle(); 192 res.putLong(DocumentsContract.METADATA_TREE_COUNT, treeCount.value); 193 res.putLong(DocumentsContract.METADATA_TREE_SIZE, treeSize.value); 194 return res; 195 } 196 197 if (!file.isFile()) { 198 Log.w(TAG, "Can't stream non-regular file. Returning empty metadata."); 199 return null; 200 } 201 if (!file.canRead()) { 202 Log.w(TAG, "Can't stream non-readable file. Returning empty metadata."); 203 return null; 204 } 205 if (!MetadataReader.isSupportedMimeType(mimeType)) { 206 Log.w(TAG, "Unsupported type " + mimeType + ". Returning empty metadata."); 207 return null; 208 } 209 210 InputStream stream = null; 211 try { 212 Bundle metadata = new Bundle(); 213 stream = new FileInputStream(file.getAbsolutePath()); 214 MetadataReader.getMetadata(metadata, stream, mimeType, null); 215 return metadata; 216 } catch (IOException e) { 217 Log.e(TAG, "An error occurred retrieving the metadata", e); 218 return null; 219 } finally { 220 IoUtils.closeQuietly(stream); 221 } 222 } 223 findDocumentPath(File parent, File doc)224 protected final List<String> findDocumentPath(File parent, File doc) 225 throws FileNotFoundException { 226 227 if (!doc.exists()) { 228 throw new FileNotFoundException(doc + " is not found."); 229 } 230 231 if (!FileUtils.contains(parent, doc)) { 232 throw new FileNotFoundException(doc + " is not found under " + parent); 233 } 234 235 LinkedList<String> path = new LinkedList<>(); 236 while (doc != null && FileUtils.contains(parent, doc)) { 237 path.addFirst(getDocIdForFile(doc)); 238 239 doc = doc.getParentFile(); 240 } 241 242 return path; 243 } 244 245 @Override createDocument(String docId, String mimeType, String displayName)246 public String createDocument(String docId, String mimeType, String displayName) 247 throws FileNotFoundException { 248 displayName = FileUtils.buildValidFatFilename(displayName); 249 250 final File parent = getFileForDocId(docId); 251 if (!parent.isDirectory()) { 252 throw new IllegalArgumentException("Parent document isn't a directory"); 253 } 254 255 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); 256 final String childId; 257 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 258 if (!file.mkdir()) { 259 throw new IllegalStateException("Failed to mkdir " + file); 260 } 261 childId = getDocIdForFile(file); 262 onDocIdChanged(childId); 263 } else { 264 try { 265 if (!file.createNewFile()) { 266 throw new IllegalStateException("Failed to touch " + file); 267 } 268 childId = getDocIdForFile(file); 269 onDocIdChanged(childId); 270 } catch (IOException e) { 271 throw new IllegalStateException("Failed to touch " + file + ": " + e); 272 } 273 } 274 updateMediaStore(getContext(), file); 275 return childId; 276 } 277 278 @Override renameDocument(String docId, String displayName)279 public String renameDocument(String docId, String displayName) throws FileNotFoundException { 280 // Since this provider treats renames as generating a completely new 281 // docId, we're okay with letting the MIME type change. 282 displayName = FileUtils.buildValidFatFilename(displayName); 283 284 final File before = getFileForDocId(docId); 285 final File beforeVisibleFile = getFileForDocId(docId, true); 286 final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName); 287 if (!before.renameTo(after)) { 288 throw new IllegalStateException("Failed to rename to " + after); 289 } 290 291 final String afterDocId = getDocIdForFile(after); 292 onDocIdChanged(docId); 293 onDocIdDeleted(docId); 294 onDocIdChanged(afterDocId); 295 296 final File afterVisibleFile = getFileForDocId(afterDocId, true); 297 298 updateMediaStore(getContext(), beforeVisibleFile); 299 updateMediaStore(getContext(), afterVisibleFile); 300 301 if (!TextUtils.equals(docId, afterDocId)) { 302 return afterDocId; 303 } else { 304 return null; 305 } 306 } 307 308 @Override moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)309 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 310 String targetParentDocumentId) 311 throws FileNotFoundException { 312 final File before = getFileForDocId(sourceDocumentId); 313 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); 314 final File visibleFileBefore = getFileForDocId(sourceDocumentId, true); 315 316 if (after.exists()) { 317 throw new IllegalStateException("Already exists " + after); 318 } 319 if (!before.renameTo(after)) { 320 throw new IllegalStateException("Failed to move to " + after); 321 } 322 323 final String docId = getDocIdForFile(after); 324 onDocIdChanged(sourceDocumentId); 325 onDocIdDeleted(sourceDocumentId); 326 onDocIdChanged(docId); 327 // update the database 328 updateMediaStore(getContext(), visibleFileBefore); 329 updateMediaStore(getContext(), getFileForDocId(docId, true)); 330 return docId; 331 } 332 updateMediaStore(@onNull Context context, File file)333 private static void updateMediaStore(@NonNull Context context, File file) { 334 if (file != null) { 335 final ContentResolver resolver = context.getContentResolver(); 336 final String noMedia = ".nomedia"; 337 // For file, check whether the file name is .nomedia or not. 338 // If yes, scan the parent directory to update all files in the directory. 339 if (!file.isDirectory() && file.getName().toLowerCase(Locale.ROOT).endsWith(noMedia)) { 340 MediaStore.scanFile(resolver, file.getParentFile()); 341 } else { 342 MediaStore.scanFile(resolver, file); 343 } 344 } 345 } 346 347 @Override deleteDocument(String docId)348 public void deleteDocument(String docId) throws FileNotFoundException { 349 final File file = getFileForDocId(docId); 350 final File visibleFile = getFileForDocId(docId, true); 351 352 final boolean isDirectory = file.isDirectory(); 353 if (isDirectory) { 354 FileUtils.deleteContents(file); 355 } 356 // We could be deleting pending media which doesn't have any content yet, so only throw 357 // if the file exists and we fail to delete it. 358 if (file.exists() && !file.delete()) { 359 throw new IllegalStateException("Failed to delete " + file); 360 } 361 362 onDocIdChanged(docId); 363 onDocIdDeleted(docId); 364 updateMediaStore(getContext(), visibleFile); 365 } 366 367 @Override queryDocument(String documentId, String[] projection)368 public Cursor queryDocument(String documentId, String[] projection) 369 throws FileNotFoundException { 370 final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); 371 includeFile(result, documentId, null); 372 return result; 373 } 374 375 /** 376 * This method is similar to 377 * {@link DocumentsProvider#queryChildDocuments(String, String[], String)}. This method returns 378 * all children documents including hidden directories/files. 379 * 380 * <p> 381 * In a scoped storage world, access to "Android/data" style directories are hidden for privacy 382 * reasons. This method may show privacy sensitive data, so its usage should only be in 383 * restricted modes. 384 * 385 * @param parentDocumentId the directory to return children for. 386 * @param projection list of {@link Document} columns to put into the 387 * cursor. If {@code null} all supported columns should be 388 * included. 389 * @param sortOrder how to order the rows, formatted as an SQL 390 * {@code ORDER BY} clause (excluding the ORDER BY itself). 391 * Passing {@code null} will use the default sort order, which 392 * may be unordered. This ordering is a hint that can be used to 393 * prioritize how data is fetched from the network, but UI may 394 * always enforce a specific ordering 395 * @throws FileNotFoundException when parent document doesn't exist or query fails 396 */ queryChildDocumentsShowAll( String parentDocumentId, String[] projection, String sortOrder)397 protected Cursor queryChildDocumentsShowAll( 398 String parentDocumentId, String[] projection, String sortOrder) 399 throws FileNotFoundException { 400 return queryChildDocuments(parentDocumentId, projection, sortOrder, File -> true); 401 } 402 403 @Override queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)404 public Cursor queryChildDocuments( 405 String parentDocumentId, String[] projection, String sortOrder) 406 throws FileNotFoundException { 407 // Access to some directories is hidden for privacy reasons. 408 return queryChildDocuments(parentDocumentId, projection, sortOrder, this::shouldShow); 409 } 410 queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder, @NonNull Predicate<File> filter)411 private Cursor queryChildDocuments( 412 String parentDocumentId, String[] projection, String sortOrder, 413 @NonNull Predicate<File> filter) throws FileNotFoundException { 414 final File parent = getFileForDocId(parentDocumentId); 415 final MatrixCursor result = new DirectoryCursor( 416 resolveProjection(projection), parentDocumentId, parent); 417 if (parent.isDirectory()) { 418 for (File file : FileUtils.listFilesOrEmpty(parent)) { 419 if (filter.test(file)) { 420 includeFile(result, null, file); 421 } 422 } 423 } else { 424 Log.w(TAG, "parentDocumentId '" + parentDocumentId + "' is not Directory"); 425 } 426 return result; 427 } 428 429 /** 430 * Searches documents under the given folder. 431 * 432 * To avoid runtime explosion only returns the at most 23 items. 433 * 434 * @param folder the root folder where recursive search begins 435 * @param query the search condition used to match file names 436 * @param projection projection of the returned cursor 437 * @param exclusion absolute file paths to exclude from result 438 * @param queryArgs the query arguments for search 439 * @return cursor containing search result. Include 440 * {@link ContentResolver#EXTRA_HONORED_ARGS} in {@link Cursor} 441 * extras {@link Bundle} when any QUERY_ARG_* value was honored 442 * during the preparation of the results. 443 * @throws FileNotFoundException when root folder doesn't exist or search fails 444 * 445 * @see ContentResolver#EXTRA_HONORED_ARGS 446 */ querySearchDocuments( File folder, String[] projection, Set<String> exclusion, Bundle queryArgs)447 protected final Cursor querySearchDocuments( 448 File folder, String[] projection, Set<String> exclusion, Bundle queryArgs) 449 throws FileNotFoundException { 450 final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); 451 final LinkedList<File> pending = new LinkedList<>(); 452 pending.add(folder); 453 while (!pending.isEmpty() && result.getCount() < 24) { 454 final File file = pending.removeFirst(); 455 if (shouldHide(file)) continue; 456 457 if (file.isDirectory()) { 458 for (File child : FileUtils.listFilesOrEmpty(file)) { 459 pending.add(child); 460 } 461 } 462 if (!exclusion.contains(file.getAbsolutePath()) && matchSearchQueryArguments(file, 463 queryArgs)) { 464 includeFile(result, null, file); 465 } 466 } 467 468 final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs); 469 if (handledQueryArgs.length > 0) { 470 final Bundle extras = new Bundle(); 471 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs); 472 result.setExtras(extras); 473 } 474 return result; 475 } 476 477 @Override getDocumentType(String documentId)478 public String getDocumentType(String documentId) throws FileNotFoundException { 479 return getDocumentType(documentId, getFileForDocId(documentId)); 480 } 481 getDocumentType(final String documentId, final File file)482 private String getDocumentType(final String documentId, final File file) 483 throws FileNotFoundException { 484 if (file.isDirectory()) { 485 return Document.MIME_TYPE_DIR; 486 } else { 487 final int lastDot = documentId.lastIndexOf('.'); 488 if (lastDot >= 0) { 489 final String extension = documentId.substring(lastDot + 1).toLowerCase(); 490 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 491 if (mime != null) { 492 return mime; 493 } 494 } 495 return ContentResolver.MIME_TYPE_DEFAULT; 496 } 497 } 498 499 @Override openDocument( String documentId, String mode, CancellationSignal signal)500 public ParcelFileDescriptor openDocument( 501 String documentId, String mode, CancellationSignal signal) 502 throws FileNotFoundException { 503 final File file = getFileForDocId(documentId); 504 final File visibleFile = getFileForDocId(documentId, true); 505 506 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 507 if (visibleFile == null) { 508 return ParcelFileDescriptor.open(file, pfdMode); 509 } else if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) { 510 return openFileForRead(visibleFile); 511 } else { 512 try { 513 // When finished writing, kick off media scanner 514 return ParcelFileDescriptor.open( 515 file, pfdMode, mHandler, (IOException e) -> { 516 onDocIdChanged(documentId); 517 scanFile(visibleFile); 518 }); 519 } catch (IOException e) { 520 throw new FileNotFoundException("Failed to open for writing: " + e); 521 } 522 } 523 } 524 openFileForRead(final File target)525 private ParcelFileDescriptor openFileForRead(final File target) throws FileNotFoundException { 526 final Uri uri = MediaStore.scanFile(getContext().getContentResolver(), target); 527 if (uri == null) { 528 Log.w(TAG, "Failed to retrieve media store URI for: " + target); 529 return ParcelFileDescriptor.open(target, ParcelFileDescriptor.MODE_READ_ONLY); 530 } 531 532 // Passing the calling uid via EXTRA_MEDIA_CAPABILITIES_UID, so that the decision to 533 // transcode or not transcode can be made based upon the calling app's uid, and not based 534 // upon the Provider's uid. 535 final Bundle opts = new Bundle(); 536 opts.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID, Binder.getCallingUid()); 537 538 final AssetFileDescriptor afd = 539 getContext().getContentResolver().openTypedAssetFileDescriptor(uri, "*/*", opts); 540 if (afd == null) { 541 Log.w(TAG, "Failed to open with media_capabilities uid for URI: " + uri); 542 return ParcelFileDescriptor.open(target, ParcelFileDescriptor.MODE_READ_ONLY); 543 } 544 545 return afd.getParcelFileDescriptor(); 546 } 547 548 /** 549 * Test if the file matches the query arguments. 550 * 551 * @param file the file to test 552 * @param queryArgs the query arguments 553 */ matchSearchQueryArguments(File file, Bundle queryArgs)554 private boolean matchSearchQueryArguments(File file, Bundle queryArgs) { 555 if (file == null) { 556 return false; 557 } 558 559 final String fileMimeType; 560 final String fileName = file.getName(); 561 562 if (file.isDirectory()) { 563 fileMimeType = DocumentsContract.Document.MIME_TYPE_DIR; 564 } else { 565 int dotPos = fileName.lastIndexOf('.'); 566 if (dotPos < 0) { 567 return false; 568 } 569 final String extension = fileName.substring(dotPos + 1); 570 fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 571 } 572 return DocumentsContract.matchSearchQueryArguments(queryArgs, fileName, fileMimeType, 573 file.lastModified(), file.length()); 574 } 575 scanFile(File visibleFile)576 private void scanFile(File visibleFile) { 577 final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 578 intent.setData(Uri.fromFile(visibleFile)); 579 getContext().sendBroadcast(intent); 580 } 581 582 @Override openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)583 public AssetFileDescriptor openDocumentThumbnail( 584 String documentId, Point sizeHint, CancellationSignal signal) 585 throws FileNotFoundException { 586 final File file = getFileForDocId(documentId); 587 return DocumentsContract.openImageThumbnail(file); 588 } 589 includeFile(final MatrixCursor result, String docId, File file)590 protected RowBuilder includeFile(final MatrixCursor result, String docId, File file) 591 throws FileNotFoundException { 592 final String[] columns = result.getColumnNames(); 593 final RowBuilder row = result.newRow(); 594 595 if (docId == null) { 596 docId = getDocIdForFile(file); 597 } else { 598 file = getFileForDocId(docId); 599 } 600 601 final String mimeType = getDocumentType(docId, file); 602 row.add(Document.COLUMN_DOCUMENT_ID, docId); 603 row.add(Document.COLUMN_MIME_TYPE, mimeType); 604 605 final int flagIndex = ArrayUtils.indexOf(columns, Document.COLUMN_FLAGS); 606 if (flagIndex != -1) { 607 int flags = 0; 608 if (file.canWrite()) { 609 if (mimeType.equals(Document.MIME_TYPE_DIR)) { 610 flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 611 flags |= Document.FLAG_SUPPORTS_DELETE; 612 flags |= Document.FLAG_SUPPORTS_RENAME; 613 flags |= Document.FLAG_SUPPORTS_MOVE; 614 615 if (shouldBlockFromTree(docId)) { 616 flags |= Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE; 617 } 618 619 } else { 620 flags |= Document.FLAG_SUPPORTS_WRITE; 621 flags |= Document.FLAG_SUPPORTS_DELETE; 622 flags |= Document.FLAG_SUPPORTS_RENAME; 623 flags |= Document.FLAG_SUPPORTS_MOVE; 624 } 625 } 626 627 if (mimeType.startsWith("image/")) { 628 flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 629 } 630 631 if (typeSupportsMetadata(mimeType)) { 632 flags |= Document.FLAG_SUPPORTS_METADATA; 633 } 634 row.add(flagIndex, flags); 635 } 636 637 final int displayNameIndex = ArrayUtils.indexOf(columns, Document.COLUMN_DISPLAY_NAME); 638 if (displayNameIndex != -1) { 639 row.add(displayNameIndex, file.getName()); 640 } 641 642 final int lastModifiedIndex = ArrayUtils.indexOf(columns, Document.COLUMN_LAST_MODIFIED); 643 if (lastModifiedIndex != -1) { 644 final long lastModified = file.lastModified(); 645 // Only publish dates reasonably after epoch 646 if (lastModified > 31536000000L) { 647 row.add(lastModifiedIndex, lastModified); 648 } 649 } 650 final int sizeIndex = ArrayUtils.indexOf(columns, Document.COLUMN_SIZE); 651 if (sizeIndex != -1) { 652 row.add(sizeIndex, file.length()); 653 } 654 655 // Return the row builder just in case any subclass want to add more stuff to it. 656 return row; 657 } 658 659 private static final Pattern PATTERN_HIDDEN_PATH = Pattern.compile( 660 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)$"); 661 662 /** 663 * In a scoped storage world, access to "Android/data" style directories are 664 * hidden for privacy reasons. 665 */ shouldHide(@onNull File file)666 protected boolean shouldHide(@NonNull File file) { 667 return (PATTERN_HIDDEN_PATH.matcher(file.getAbsolutePath()).matches()); 668 } 669 shouldShow(@onNull File file)670 private boolean shouldShow(@NonNull File file) { 671 return !shouldHide(file); 672 } 673 shouldBlockFromTree(@onNull String docId)674 protected boolean shouldBlockFromTree(@NonNull String docId) { 675 return false; 676 } 677 typeSupportsMetadata(String mimeType)678 protected boolean typeSupportsMetadata(String mimeType) { 679 return MetadataReader.isSupportedMimeType(mimeType) 680 || Document.MIME_TYPE_DIR.equals(mimeType); 681 } 682 getFileForDocId(String docId)683 protected final File getFileForDocId(String docId) throws FileNotFoundException { 684 return getFileForDocId(docId, false); 685 } 686 resolveProjection(String[] projection)687 private String[] resolveProjection(String[] projection) { 688 return projection == null ? mDefaultProjection : projection; 689 } 690 startObserving(File file, Uri notifyUri, DirectoryCursor cursor)691 private void startObserving(File file, Uri notifyUri, DirectoryCursor cursor) { 692 synchronized (mObservers) { 693 DirectoryObserver observer = mObservers.get(file); 694 if (observer == null) { 695 observer = 696 new DirectoryObserver(file, getContext().getContentResolver(), notifyUri); 697 observer.startWatching(); 698 mObservers.put(file, observer); 699 } 700 observer.mCursors.add(cursor); 701 702 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 703 } 704 } 705 stopObserving(File file, DirectoryCursor cursor)706 private void stopObserving(File file, DirectoryCursor cursor) { 707 synchronized (mObservers) { 708 DirectoryObserver observer = mObservers.get(file); 709 if (observer == null) return; 710 711 observer.mCursors.remove(cursor); 712 if (observer.mCursors.size() == 0) { 713 mObservers.remove(file); 714 observer.stopWatching(); 715 } 716 717 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 718 } 719 } 720 721 private static class DirectoryObserver extends FileObserver { 722 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 723 | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 724 725 private final File mFile; 726 private final ContentResolver mResolver; 727 private final Uri mNotifyUri; 728 private final CopyOnWriteArrayList<DirectoryCursor> mCursors; 729 DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri)730 DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 731 super(file.getAbsolutePath(), NOTIFY_EVENTS); 732 mFile = file; 733 mResolver = resolver; 734 mNotifyUri = notifyUri; 735 mCursors = new CopyOnWriteArrayList<>(); 736 } 737 738 @Override onEvent(int event, String path)739 public void onEvent(int event, String path) { 740 if ((event & NOTIFY_EVENTS) != 0) { 741 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 742 for (DirectoryCursor cursor : mCursors) { 743 cursor.notifyChanged(); 744 } 745 mResolver.notifyChange(mNotifyUri, null, false); 746 } 747 } 748 749 @Override toString()750 public String toString() { 751 String filePath = mFile.getAbsolutePath(); 752 return "DirectoryObserver{file=" + filePath + ", ref=" + mCursors.size() + "}"; 753 } 754 } 755 756 private class DirectoryCursor extends MatrixCursor { 757 private final File mFile; 758 DirectoryCursor(String[] columnNames, String docId, File file)759 public DirectoryCursor(String[] columnNames, String docId, File file) { 760 super(columnNames); 761 762 final Uri notifyUri = buildNotificationUri(docId); 763 boolean registerSelfObserver = false; // Our FileObserver sees all relevant changes. 764 setNotificationUris(getContext().getContentResolver(), Arrays.asList(notifyUri), 765 getContext().getContentResolver().getUserId(), registerSelfObserver); 766 767 mFile = file; 768 startObserving(mFile, notifyUri, this); 769 } 770 notifyChanged()771 public void notifyChanged() { 772 onChange(false); 773 } 774 775 @Override close()776 public void close() { 777 super.close(); 778 stopObserving(mFile, this); 779 } 780 } 781 } 782