Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fc93ee6f authored by Bo Majewski's avatar Bo Majewski
Browse files

[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
parent 3fd4aa0e
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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
+5 −2
Original line number Diff line number Diff line
@@ -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
+8 −0
Original line number Diff line number Diff line
@@ -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 {
+14 −7
Original line number Diff line number Diff line
@@ -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
    }

+51 −3
Original line number Diff line number Diff line
@@ -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
Loading