6262import java .nio .file .Files ;
6363import java .nio .file .Path ;
6464import java .nio .file .attribute .BasicFileAttributes ;
65+ import java .util .ArrayDeque ;
6566import java .util .Arrays ;
6667import java .util .LinkedList ;
6768import java .util .List ;
6869import java .util .Locale ;
70+ import java .util .Queue ;
6971import java .util .Set ;
7072import java .util .concurrent .CopyOnWriteArrayList ;
71- import java .util .function .Predicate ;
72- import java .util .regex .Pattern ;
7373
7474/**
7575 * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local
@@ -87,6 +87,8 @@ public abstract class FileSystemProvider extends DocumentsProvider {
8787 DocumentsContract .QUERY_ARG_LAST_MODIFIED_AFTER ,
8888 DocumentsContract .QUERY_ARG_MIME_TYPES );
8989
90+ private static final int MAX_RESULTS_NUMBER = 23 ;
91+
9092 private static String joinNewline (String ... args ) {
9193 return TextUtils .join ("\n " , args );
9294 }
@@ -373,62 +375,53 @@ public Cursor queryDocument(String documentId, String[] projection)
373375 }
374376
375377 /**
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
378+ * WARNING: this method should really be {@code final}, but for the backward compatibility it's
379+ * not; new classes that extend {@link FileSystemProvider} should override
380+ * {@link #queryChildDocuments(String, String[], String, boolean)}, not this method.
396381 */
397- protected Cursor queryChildDocumentsShowAll (
398- String parentDocumentId , String [] projection , String sortOrder )
382+ @ Override
383+ public Cursor queryChildDocuments ( String documentId , String [] projection , String sortOrder )
399384 throws FileNotFoundException {
400- return queryChildDocuments (parentDocumentId , projection , sortOrder , File -> true );
385+ return queryChildDocuments (documentId , projection , sortOrder , /* includeHidden */ false );
401386 }
402387
388+ /**
389+ * This method is similar to {@link #queryChildDocuments(String, String[], String)}, however, it
390+ * could return <b>all</b> content of the directory, <b>including restricted (hidden)
391+ * directories and files</b>.
392+ * <p>
393+ * In the scoped storage world, some directories and files (e.g. {@code Android/data/} and
394+ * {@code Android/obb/} on the external storage) are hidden for privacy reasons.
395+ * Hence, this method may reveal privacy-sensitive data, thus should be used with extra care.
396+ */
403397 @ Override
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 );
398+ public final Cursor queryChildDocumentsForManage (String documentId , String [] projection ,
399+ String sortOrder ) throws FileNotFoundException {
400+ return queryChildDocuments (documentId , projection , sortOrder , /* includeHidden */ true );
409401 }
410402
411- private Cursor queryChildDocuments (
412- String parentDocumentId , String [] projection , String sortOrder ,
413- @ NonNull Predicate <File > filter ) throws FileNotFoundException {
414- final File parent = getFileForDocId (parentDocumentId );
403+ protected Cursor queryChildDocuments (String documentId , String [] projection , String sortOrder ,
404+ boolean includeHidden ) throws FileNotFoundException {
405+ final File parent = getFileForDocId (documentId );
415406 final MatrixCursor result = new DirectoryCursor (
416- resolveProjection (projection ), parentDocumentId , parent );
407+ resolveProjection (projection ), documentId , parent );
408+
409+ if (!parent .isDirectory ()) {
410+ Log .w (TAG , '"' + documentId + "\" is not a directory" );
411+ return result ;
412+ }
417413
418- if (!filter . test ( parent )) {
419- Log .w (TAG , "No permission to access parentDocumentId: " + parentDocumentId );
414+ if (!includeHidden && shouldHideDocument ( documentId )) {
415+ Log .w (TAG , "Queried directory \" " + documentId + " \" is hidden" );
420416 return result ;
421417 }
422418
423- if (parent .isDirectory ()) {
424- for (File file : FileUtils .listFilesOrEmpty (parent )) {
425- if (filter .test (file )) {
426- includeFile (result , null , file );
427- }
428- }
429- } else {
430- Log .w (TAG , "parentDocumentId '" + parentDocumentId + "' is not Directory" );
419+ for (File file : FileUtils .listFilesOrEmpty (parent )) {
420+ if (!includeHidden && shouldHideDocument (file )) continue ;
421+
422+ includeFile (result , null , file );
431423 }
424+
432425 return result ;
433426 }
434427
@@ -450,23 +443,29 @@ private Cursor queryChildDocuments(
450443 *
451444 * @see ContentResolver#EXTRA_HONORED_ARGS
452445 */
453- protected final Cursor querySearchDocuments (
454- File folder , String [] projection , Set <String > exclusion , Bundle queryArgs )
455- throws FileNotFoundException {
446+ protected final Cursor querySearchDocuments (File folder , String [] projection ,
447+ Set <String > exclusion , Bundle queryArgs ) throws FileNotFoundException {
456448 final MatrixCursor result = new MatrixCursor (resolveProjection (projection ));
457- final LinkedList <File > pending = new LinkedList <>();
458- pending .add (folder );
459- while (!pending .isEmpty () && result .getCount () < 24 ) {
460- final File file = pending .removeFirst ();
461- if (shouldHide (file )) continue ;
449+
450+ // We'll be a running a BFS here.
451+ final Queue <File > pending = new ArrayDeque <>();
452+ pending .offer (folder );
453+
454+ while (!pending .isEmpty () && result .getCount () < MAX_RESULTS_NUMBER ) {
455+ final File file = pending .poll ();
456+
457+ // Skip hidden documents (both files and directories)
458+ if (shouldHideDocument (file )) continue ;
462459
463460 if (file .isDirectory ()) {
464461 for (File child : FileUtils .listFilesOrEmpty (file )) {
465- pending .add (child );
462+ pending .offer (child );
466463 }
467464 }
468- if (!exclusion .contains (file .getAbsolutePath ()) && matchSearchQueryArguments (file ,
469- queryArgs )) {
465+
466+ if (exclusion .contains (file .getAbsolutePath ())) continue ;
467+
468+ if (matchSearchQueryArguments (file , queryArgs )) {
470469 includeFile (result , null , file );
471470 }
472471 }
@@ -610,26 +609,23 @@ protected RowBuilder includeFile(final MatrixCursor result, String docId, File f
610609
611610 final int flagIndex = ArrayUtils .indexOf (columns , Document .COLUMN_FLAGS );
612611 if (flagIndex != -1 ) {
612+ final boolean isDir = mimeType .equals (Document .MIME_TYPE_DIR );
613613 int flags = 0 ;
614614 if (file .canWrite ()) {
615- if (mimeType .equals (Document .MIME_TYPE_DIR )) {
615+ flags |= Document .FLAG_SUPPORTS_DELETE ;
616+ flags |= Document .FLAG_SUPPORTS_RENAME ;
617+ flags |= Document .FLAG_SUPPORTS_MOVE ;
618+ if (isDir ) {
616619 flags |= Document .FLAG_DIR_SUPPORTS_CREATE ;
617- flags |= Document .FLAG_SUPPORTS_DELETE ;
618- flags |= Document .FLAG_SUPPORTS_RENAME ;
619- flags |= Document .FLAG_SUPPORTS_MOVE ;
620-
621- if (shouldBlockFromTree (docId )) {
622- flags |= Document .FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE ;
623- }
624-
625620 } else {
626621 flags |= Document .FLAG_SUPPORTS_WRITE ;
627- flags |= Document .FLAG_SUPPORTS_DELETE ;
628- flags |= Document .FLAG_SUPPORTS_RENAME ;
629- flags |= Document .FLAG_SUPPORTS_MOVE ;
630622 }
631623 }
632624
625+ if (isDir && shouldBlockDirectoryFromTree (docId )) {
626+ flags |= Document .FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE ;
627+ }
628+
633629 if (mimeType .startsWith ("image/" )) {
634630 flags |= Document .FLAG_SUPPORTS_THUMBNAIL ;
635631 }
@@ -662,22 +658,36 @@ protected RowBuilder includeFile(final MatrixCursor result, String docId, File f
662658 return row ;
663659 }
664660
665- private static final Pattern PATTERN_HIDDEN_PATH = Pattern .compile (
666- "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)$" );
667-
668661 /**
669- * In a scoped storage world, access to "Android/data" style directories are
670- * hidden for privacy reasons.
662+ * Some providers may want to restrict access to certain directories and files,
663+ * e.g. <i>"Android/data"</i> and <i>"Android/obb"</i> on the shared storage for
664+ * privacy reasons.
665+ * Such providers should override this method.
671666 */
672- protected boolean shouldHide (@ NonNull File file ) {
673- return (PATTERN_HIDDEN_PATH .matcher (file .getAbsolutePath ()).matches ());
667+ protected boolean shouldHideDocument (@ NonNull String documentId )
668+ throws FileNotFoundException {
669+ return false ;
674670 }
675671
676- private boolean shouldShow (@ NonNull File file ) {
677- return !shouldHide (file );
672+ /**
673+ * A variant of the {@link #shouldHideDocument(String)} that takes a {@link File} instead of
674+ * a {@link String} {@code documentId}.
675+ *
676+ * @see #shouldHideDocument(String)
677+ */
678+ protected final boolean shouldHideDocument (@ NonNull File document )
679+ throws FileNotFoundException {
680+ return shouldHideDocument (getDocIdForFile (document ));
678681 }
679682
680- protected boolean shouldBlockFromTree (@ NonNull String docId ) {
683+ /**
684+ * @return if the directory that should be blocked from being selected when the user launches
685+ * an {@link Intent#ACTION_OPEN_DOCUMENT_TREE} intent.
686+ *
687+ * @see Document#FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE
688+ */
689+ protected boolean shouldBlockDirectoryFromTree (@ NonNull String documentId )
690+ throws FileNotFoundException {
681691 return false ;
682692 }
683693
0 commit comments