diff options
author | 2025-02-14 06:41:26 +0000 | |
---|---|---|
committer | 2025-02-20 17:59:53 -0800 | |
commit | fc93ee6fbd8eb32cc4239dd5c4de8d225ff729c8 (patch) | |
tree | b8b69b68d008a547e1d3ebba7120a7655e3b1c3d | |
parent | 3fd4aa0e3616842e36ecd570386b2666779b0fb1 (diff) |
[DocsUI, Search]: Enable filtering by MIME type.
Adds filtering by MIME type to the loaders. Rearranges the order of
how filtering is done to make them identical in the folder loader and
search loader.
Expands the functionality of the TestDocumentsProvider to add fildering
by MIME types and the last modified time. Adds unit tests to cover the
newly added functionality.
Test: m DocumentsUIGoogle
Bug: 378590632
Flag: com.android.documentsui.flags.use_search_v2_rw
Change-Id: I4077853d85349814720bb3d4db9dcc9e0c96bc68
8 files changed, 115 insertions, 27 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index 97f5de679..619162f90 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -1018,7 +1018,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA ? RecentsLoader.MAX_DOCS_FROM_ROOT : MAX_RESULTS; QueryOptions options = new QueryOptions( maxResults, lastModifiedDelta, Duration.ofMillis(MAX_SEARCH_TIME_MS), - mState.showHiddenFiles, mState.acceptMimes); + mState.showHiddenFiles, mState.acceptMimes, mSearchMgr.buildQueryArgs()); if (stack.isRecents() || mSearchMgr.isSearching()) { Log.d(TAG, "Creating search loader V2"); @@ -1027,7 +1027,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA final LockingContentObserver observer = new LockingContentObserver( mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); Collection<RootInfo> rootList = new ArrayList<>(); - if (root == null || root.isRecents()) { + if (stack.isRecents()) { // TODO(b:381346575): Pass roots based on user selection. rootList.addAll(mProviders.getMatchingRootsBlocking(mState).stream().filter( r -> r.supportsSearch() && r.authority != null diff --git a/src/com/android/documentsui/loaders/FolderLoader.kt b/src/com/android/documentsui/loaders/FolderLoader.kt index 2bfcd895d..a166ca752 100644 --- a/src/com/android/documentsui/loaders/FolderLoader.kt +++ b/src/com/android/documentsui/loaders/FolderLoader.kt @@ -61,15 +61,18 @@ class FolderLoader( mListedDir.documentId ) var cursor = - queryLocation(mRoot.rootId, folderChildrenUri, null, ALL_RESULTS) ?: emptyCursor() + queryLocation(mRoot.rootId, folderChildrenUri, mOptions.otherQueryArgs, ALL_RESULTS) + ?: emptyCursor() + cursor.registerContentObserver(mObserver) + val filteredCursor = FilteringCursorWrapper(cursor) filteredCursor.filterHiddenFiles(mOptions.showHidden) + filteredCursor.filterMimes(mOptions.acceptableMimeTypes, null) if (rejectBeforeTimestamp > 0L) { filteredCursor.filterLastModified(rejectBeforeTimestamp) } // TODO(b:380945065): Add filtering by category, such as images, audio, video. val sortedCursor = mSortModel.sortCursor(filteredCursor, mMimeTypeLookup) - sortedCursor.registerContentObserver(mObserver) val result = DirectoryResult() result.doc = mListedDir diff --git a/src/com/android/documentsui/loaders/QueryOptions.kt b/src/com/android/documentsui/loaders/QueryOptions.kt index 1e098b288..385815e99 100644 --- a/src/com/android/documentsui/loaders/QueryOptions.kt +++ b/src/com/android/documentsui/loaders/QueryOptions.kt @@ -15,6 +15,7 @@ */ package com.android.documentsui.loaders +import android.os.Bundle import java.time.Duration /** @@ -30,6 +31,12 @@ const val ALL_RESULTS: Int = -1 * - maximum time the query should return, including empty, results; pass null for no limits. * - whether or not to show hidden files. * - A list of MIME types used to filter returned files. + * - "Other" query arguments not covered by the above. + * + * The "other" query arguments are added as due to existing code communicating information such + * as acceptable file kind (images, videos, etc.) is done via Bundle arguments. This could be + * and should be changed if this code ever is rewritten. + * TODO(b:397095797): Merge otherQueryArgs with acceptableMimeTypes and maxLastModifiedDelta. */ data class QueryOptions( val maxResults: Int, @@ -37,6 +44,7 @@ data class QueryOptions( val maxQueryTime: Duration?, val showHidden: Boolean, val acceptableMimeTypes: Array<String>, + val otherQueryArgs: Bundle, ) { override fun equals(other: Any?): Boolean { diff --git a/src/com/android/documentsui/loaders/SearchLoader.kt b/src/com/android/documentsui/loaders/SearchLoader.kt index 6ccea7493..567363697 100644 --- a/src/com/android/documentsui/loaders/SearchLoader.kt +++ b/src/com/android/documentsui/loaders/SearchLoader.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.os.Bundle import android.provider.DocumentsContract import android.provider.DocumentsContract.Document +import android.text.TextUtils import android.util.Log import com.android.documentsui.DirectoryResult import com.android.documentsui.LockingContentObserver @@ -173,22 +174,27 @@ class SearchLoader( Log.d(TAG, "Search complete with ${cursorList.size} cursors collected") // Step 5: Assign the cursor, after adding filtering and sorting, to the results. - val filteringCursor = FilteringCursorWrapper(toSingleCursor(cursorList)) + val mergedCursor = toSingleCursor(cursorList) + mergedCursor.registerContentObserver(mObserver) + val filteringCursor = FilteringCursorWrapper(mergedCursor) filteringCursor.filterHiddenFiles(mOptions.showHidden) + filteringCursor.filterMimes( + mOptions.acceptableMimeTypes, + if (TextUtils.isEmpty(mQuery)) arrayOf<String>(Document.MIME_TYPE_DIR) else null + ) if (rejectBeforeTimestamp > 0L) { filteringCursor.filterLastModified(rejectBeforeTimestamp) } - filteringCursor.filterMimes(mOptions.acceptableMimeTypes, arrayOf(Document.MIME_TYPE_DIR)) - val sortingCursor = mSortModel.sortCursor(filteringCursor, mMimeTypeLookup) - sortingCursor.registerContentObserver(mObserver) - result.cursor = sortingCursor + result.cursor = mSortModel.sortCursor(filteringCursor, mMimeTypeLookup) // TODO(b:388336095): Record the total time it took to complete search. return result } private fun createContentProviderQuery(root: RootInfo) = - if (mQuery == null || mQuery.isBlank()) { + if (TextUtils.isEmpty(mQuery) && mOptions.otherQueryArgs.isEmpty) { + // NOTE: recent document URI does not respect query-arg-mime-types restrictions. Thus + // we only create the recents URI if both the query and other args are empty. DocumentsContract.buildRecentDocumentsUri( root.authority, root.rootId @@ -211,9 +217,10 @@ class SearchLoader( rejectBeforeTimestamp ) } - if (mQuery != null && !mQuery.isBlank()) { + if (!TextUtils.isEmpty(mQuery)) { queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, mQuery) } + queryArgs.putAll(mOptions.otherQueryArgs) return queryArgs } diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java index 5c5ed856f..c7e884fde 100644 --- a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java +++ b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java @@ -17,6 +17,7 @@ package com.android.documentsui.testing; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.pm.ProviderInfo; import android.database.Cursor; @@ -25,6 +26,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsProvider; @@ -91,13 +93,59 @@ public class TestDocumentsProvider extends DocumentsProvider { return mNextRecentDocuments; } + private String getStringColumn(Cursor cursor, String name) { + return cursor.getString(cursor.getColumnIndexOrThrow(name)); + } + + private long getLongColumn(Cursor cursor, String name) { + return cursor.getLong(cursor.getColumnIndexOrThrow(name)); + } + + @Override + public Cursor querySearchDocuments(@NonNull String rootId, @Nullable String[] projection, + @NonNull Bundle queryArgs) { + TestCursor cursor = new TestCursor(DOCUMENTS_PROJECTION); + if (mNextChildDocuments == null) { + return cursor; + } + for (boolean hasNext = mNextChildDocuments.moveToFirst(); hasNext; + hasNext = mNextChildDocuments.moveToNext()) { + String displayName = getStringColumn(mNextChildDocuments, Document.COLUMN_DISPLAY_NAME); + String mimeType = getStringColumn(mNextChildDocuments, Document.COLUMN_MIME_TYPE); + long lastModified = getLongColumn(mNextChildDocuments, Document.COLUMN_LAST_MODIFIED); + long size = getLongColumn(mNextChildDocuments, Document.COLUMN_SIZE); + + if (DocumentsContract.matchSearchQueryArguments(queryArgs, displayName, mimeType, + lastModified, size)) { + cursor.newRow() + .add(Document.COLUMN_DOCUMENT_ID, + getStringColumn(mNextChildDocuments, Document.COLUMN_DOCUMENT_ID)) + .add(Document.COLUMN_MIME_TYPE, + getStringColumn(mNextChildDocuments, Document.COLUMN_MIME_TYPE)) + .add(Document.COLUMN_DISPLAY_NAME, + getStringColumn(mNextChildDocuments, Document.COLUMN_DISPLAY_NAME)) + .add(Document.COLUMN_LAST_MODIFIED, + getLongColumn(mNextChildDocuments, Document.COLUMN_LAST_MODIFIED)) + .add(Document.COLUMN_FLAGS, + getLongColumn(mNextChildDocuments, Document.COLUMN_FLAGS)) + .add(Document.COLUMN_SUMMARY, + getStringColumn(mNextChildDocuments, Document.COLUMN_SUMMARY)) + .add(Document.COLUMN_SIZE, + getLongColumn(mNextChildDocuments, Document.COLUMN_SIZE)) + .add(Document.COLUMN_ICON, + getLongColumn(mNextChildDocuments, Document.COLUMN_ICON)); + } + } + return cursor; + } + @Override public Cursor querySearchDocuments(String rootId, String query, String[] projection) { - if (mNextChildDocuments != null) { - return filterCursorByString(mNextChildDocuments, query); + if (mNextChildDocuments == null) { + return null; } - return mNextChildDocuments; + return filterCursorByString(mNextChildDocuments, query); } @Override diff --git a/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt index 62434b71f..55f83bfea 100644 --- a/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt +++ b/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt @@ -15,6 +15,7 @@ */ package com.android.documentsui.loaders +import android.os.Bundle import android.os.Parcel import android.provider.DocumentsContract import com.android.documentsui.DirectoryResult @@ -47,8 +48,10 @@ data class LoaderTestParams( val query: String, // The delta from now that indicates maximum age of matched files. val lastModifiedDelta: Duration?, + // The extra arguments typically supplied by search view manager. + val otherArgs: Bundle, // The number of files that are expected, for the above parameters, to be found by a loader. - val expectedCount: Int + val expectedCount: Int, ) /** diff --git a/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt index cb0735b17..146152fb6 100644 --- a/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt +++ b/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt @@ -15,6 +15,7 @@ */ package com.android.documentsui.loaders +import android.os.Bundle import androidx.test.filters.SmallTest import com.android.documentsui.ContentLock import com.android.documentsui.base.DocumentInfo @@ -36,11 +37,16 @@ class FolderLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTes @JvmStatic @Parameters(name = "with parameters {0}") fun data() = listOf( - LoaderTestParams("", null, TOTAL_FILE_COUNT), + LoaderTestParams("", null, Bundle(), TOTAL_FILE_COUNT), // The first file is at NOW, the second at NOW - 1h, etc. - LoaderTestParams("", Duration.ofMinutes(1L), 1), - LoaderTestParams("", Duration.ofMinutes(60L + 1), 2), - LoaderTestParams("", Duration.ofMinutes(TOTAL_FILE_COUNT * 60L + 1), TOTAL_FILE_COUNT), + LoaderTestParams("", Duration.ofMinutes(1L), Bundle(), 1), + LoaderTestParams("", Duration.ofMinutes(60L + 1), Bundle(), 2), + LoaderTestParams( + "", + Duration.ofMinutes(TOTAL_FILE_COUNT * 60L + 1), + Bundle(), + TOTAL_FILE_COUNT + ), ) } @@ -56,7 +62,8 @@ class FolderLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTes testParams.lastModifiedDelta, null, true, - arrayOf<String>("*/*") + arrayOf<String>("*/*"), + testParams.otherArgs, ) val contentLock = ContentLock() // TODO(majewski): Is there a better way to create Downloads root folder DocumentInfo? diff --git a/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt index 94012b7ff..4e9104626 100644 --- a/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt +++ b/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt @@ -15,6 +15,8 @@ */ package com.android.documentsui.loaders +import android.os.Bundle +import android.provider.DocumentsContract import androidx.test.filters.SmallTest import com.android.documentsui.ContentLock import com.android.documentsui.LockingContentObserver @@ -34,6 +36,12 @@ import org.junit.runners.Parameterized.Parameters private const val TOTAL_FILE_COUNT = 8 +fun createQueryArgs(vararg mimeTypes: String): Bundle { + val args = Bundle() + args.putStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES, arrayOf<String>(*mimeTypes)) + return args +} + @RunWith(Parameterized::class) @SmallTest class SearchLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTest() { @@ -45,12 +53,14 @@ class SearchLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTes @JvmStatic @Parameters(name = "with parameters {0}") fun data() = listOf( - LoaderTestParams("sample", null, TOTAL_FILE_COUNT), - LoaderTestParams("txt", null, 2), - LoaderTestParams("foozig", null, 0), + LoaderTestParams("sample", null, Bundle(), TOTAL_FILE_COUNT), + LoaderTestParams("txt", null, Bundle(), 2), + LoaderTestParams("foozig", null, Bundle(), 0), // The first file is at NOW, the second at NOW - 1h; expect 2. - LoaderTestParams("sample", Duration.ofMinutes(60 + 1), 2), - // TODO(b:378590632): Add test for recents. + LoaderTestParams("sample", Duration.ofMinutes(60 + 1), Bundle(), 2), + LoaderTestParams("sample", null, createQueryArgs("image/*"), 2), + LoaderTestParams("sample", null, createQueryArgs("image/*", "video/*"), 6), + LoaderTestParams("sample", null, createQueryArgs("application/pdf"), 0), ) } @@ -72,7 +82,8 @@ class SearchLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTes testParams.lastModifiedDelta, null, true, - arrayOf("*/*") + arrayOf("*/*"), + testParams.otherArgs, ) val rootIds = listOf(TestProvidersAccess.DOWNLOADS) @@ -101,7 +112,8 @@ class SearchLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTes fun testBlankQueryAndRecency() { val userIds = listOf(TestProvidersAccess.DOWNLOADS.userId) val rootIds = listOf(TestProvidersAccess.DOWNLOADS) - val noLastModifiedQueryOptions = QueryOptions(10, null, null, true, arrayOf("*/*")) + val noLastModifiedQueryOptions = + QueryOptions(10, null, null, true, arrayOf("*/*"), Bundle()) // Blank query and no last modified duration is invalid. assertThrows(IllegalArgumentException::class.java) { |