Loading src/com/android/documentsui/AbstractActionHandler.java +86 −2 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static com.android.documentsui.base.DocumentInfo.getCursorInt; import static com.android.documentsui.base.DocumentInfo.getCursorString; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.flags.Flags.desktopFileHandling; import static com.android.documentsui.flags.Flags.useSearchV2; import android.app.PendingIntent; import android.content.ActivityNotFoundException; Loading @@ -38,6 +39,7 @@ import android.util.Log; import android.util.Pair; import android.view.DragEvent; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager.LoaderCallbacks; Loading @@ -63,6 +65,9 @@ import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.FocusHandler; import com.android.documentsui.files.LauncherActivity; import com.android.documentsui.files.QuickViewIntentBuilder; import com.android.documentsui.loaders.FolderLoader; import com.android.documentsui.loaders.QueryOptions; import com.android.documentsui.loaders.SearchLoader; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.GetRootDocumentTask; import com.android.documentsui.roots.LoadFirstRootTask; Loading @@ -73,10 +78,14 @@ import com.android.documentsui.sorting.SortListFragment; import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.Snackbars; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.function.Consumer; Loading Loading @@ -894,16 +903,28 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { private ExecutorService mExecutorService = null; private static final long MAX_SEARCH_TIME_MS = 3000; private static final int MAX_RESULTS = 500; @NonNull @Override public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { Context context = mActivity; // If document stack is not initialized, i.e. if the root is null, create "Recents" root // with the selected user. if (!mState.stack.isInitialized()) { mState.stack.changeRoot(mActivity.getCurrentRoot()); } if (useSearchV2()) { return onCreateLoaderV2(id, args); } return onCreateLoaderV1(id, args); } private Loader<DirectoryResult> onCreateLoaderV1(int id, Bundle args) { Context context = mActivity; if (mState.stack.isRecents()) { final LockingContentObserver observer = new LockingContentObserver( mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); Loading Loading @@ -980,6 +1001,69 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } } private Loader<DirectoryResult> onCreateLoaderV2(int id, Bundle args) { if (mExecutorService == null) { // TODO(b:388130971): Fine tune the size of the thread pool. mExecutorService = Executors.newFixedThreadPool( GlobalSearchLoader.MAX_OUTSTANDING_TASK); } DocumentStack stack = mState.stack; RootInfo root = stack.getRoot(); List<UserId> userIdList = DocumentsApplication.getUserIdManager(mActivity).getUserIds(); Duration lastModifiedDelta = stack.isRecents() ? Duration.ofMillis(RecentsLoader.REJECT_OLDER_THAN) : null; int maxResults = (root == null || root.isRecents()) ? RecentsLoader.MAX_DOCS_FROM_ROOT : MAX_RESULTS; QueryOptions options = new QueryOptions( maxResults, lastModifiedDelta, Duration.ofMillis(MAX_SEARCH_TIME_MS), mState.showHiddenFiles, mState.acceptMimes); if (stack.isRecents() || mSearchMgr.isSearching()) { Log.d(TAG, "Creating search loader V2"); // For search and recent we create an observer that restart the loader every time // one of the searched content providers reports a change. final LockingContentObserver observer = new LockingContentObserver( mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); Collection<RootInfo> rootList = new ArrayList<>(); if (root == null || root.isRecents()) { // TODO(b:381346575): Pass roots based on user selection. rootList.addAll(mProviders.getMatchingRootsBlocking(mState).stream().filter( r -> r.supportsSearch() && r.authority != null && r.rootId != null).toList()); } else { rootList.add(root); } return new SearchLoader( mActivity, userIdList, mInjector.fileTypeLookup, observer, rootList, mSearchMgr.getCurrentSearch(), options, mState.sortModel, mExecutorService ); } Log.d(TAG, "Creating folder loader V2"); // For folder scan we pass the content lock to the loader so that it can register // an a callback to its internal method that forces a reload of the folder, every // time the content provider reports a change. return new FolderLoader( mActivity, userIdList, mInjector.fileTypeLookup, mContentLock, root, stack.peek(), options, mState.sortModel ); } @Override public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { if (DEBUG) { Loading src/com/android/documentsui/MultiRootDocumentsLoader.java +1 −1 Original line number Diff line number Diff line Loading @@ -71,7 +71,7 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory // previously returned cursors for filtering/sorting; this currently races // with the UI thread. private static final int MAX_OUTSTANDING_TASK = 4; public static final int MAX_OUTSTANDING_TASK = 4; private static final int MAX_OUTSTANDING_TASK_SVELTE = 2; /** Loading src/com/android/documentsui/RecentsLoader.java +3 −3 Original line number Diff line number Diff line Loading @@ -37,13 +37,13 @@ public class RecentsLoader extends MultiRootDocumentsLoader { private static final String TAG = "RecentsLoader"; /** Ignore documents older than this age. */ private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; public static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; /** MIME types that should always be excluded from recents. */ /** MIME types that should always be excluded from the Recents view. */ private static final String[] REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR}; /** Maximum documents from a single root. */ private static final int MAX_DOCS_FROM_ROOT = 64; public static final int MAX_DOCS_FROM_ROOT = 64; private final UserId mUserId; Loading src/com/android/documentsui/loaders/BaseFileLoader.kt 0 → 100644 +208 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.loaders import android.content.Context import android.database.Cursor import android.database.MatrixCursor import android.database.MergeCursor import android.net.Uri import android.os.Bundle import android.os.CancellationSignal import android.os.RemoteException import android.provider.DocumentsContract.Document import android.util.Log import androidx.loader.content.AsyncTaskLoader import com.android.documentsui.DirectoryResult import com.android.documentsui.base.Lookup import com.android.documentsui.base.UserId import com.android.documentsui.roots.RootCursorWrapper const val TAG = "SearchV2" val FILE_ENTRY_COLUMNS = arrayOf( Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SUMMARY, Document.COLUMN_SIZE, Document.COLUMN_ICON, ) fun emptyCursor(): Cursor { return MatrixCursor(FILE_ENTRY_COLUMNS) } /** * Helper function that returns a single, non-null cursor constructed from the given list of * cursors. */ fun toSingleCursor(cursorList: List<Cursor>): Cursor { if (cursorList.isEmpty()) { return emptyCursor() } if (cursorList.size == 1) { return cursorList[0] } return MergeCursor(cursorList.toTypedArray()) } /** * The base class for search and directory loaders. This class implements common functionality * shared by these loaders. The extending classes should implement loadInBackground, which * should call the queryLocation method. */ abstract class BaseFileLoader( context: Context, private val mUserIdList: List<UserId>, protected val mMimeTypeLookup: Lookup<String, String>, ) : AsyncTaskLoader<DirectoryResult>(context) { private var mSignal: CancellationSignal? = null private var mResult: DirectoryResult? = null override fun cancelLoadInBackground() { Log.d(TAG, "BasedFileLoader.cancelLoadInBackground") super.cancelLoadInBackground() synchronized(this) { mSignal?.cancel() } } override fun deliverResult(result: DirectoryResult?) { Log.d(TAG, "BasedFileLoader.deliverResult") if (isReset) { closeResult(result) return } val oldResult: DirectoryResult? = mResult mResult = result if (isStarted) { super.deliverResult(result) } if (oldResult != null && oldResult !== result) { closeResult(oldResult) } } override fun onStartLoading() { Log.d(TAG, "BasedFileLoader.onStartLoading") val isCursorStale: Boolean = checkIfCursorStale(mResult) if (mResult != null && !isCursorStale) { deliverResult(mResult) } if (takeContentChanged() || mResult == null || isCursorStale) { forceLoad() } } override fun onStopLoading() { Log.d(TAG, "BasedFileLoader.onStopLoading") cancelLoad() } override fun onCanceled(result: DirectoryResult?) { Log.d(TAG, "BasedFileLoader.onCanceled") closeResult(result) } override fun onReset() { Log.d(TAG, "BasedFileLoader.onReset") super.onReset() // Ensure the loader is stopped onStopLoading() closeResult(mResult) mResult = null } /** * Quietly closes the result cursor, if results are still available. */ fun closeResult(result: DirectoryResult?) { try { result?.close() } catch (e: Exception) { Log.d(TAG, "Failed to close result", e) } } private fun checkIfCursorStale(result: DirectoryResult?): Boolean { if (result == null) { return true } val cursor = result.cursor ?: return true if (cursor.isClosed) { return true } Log.d(TAG, "Long check of cursor staleness") val count = cursor.count if (!cursor.moveToPosition(-1)) { return true } for (i in 1..count) { if (!cursor.moveToNext()) { return true } } return false } /** * A function that, for the specified location rooted in the root with the given rootId * attempts to obtain a non-null cursor from the content provider client obtained for the * given locationUri. It returns the first non-null cursor, if one can be found, or null, * if it fails to query the given location for all known users. */ fun queryLocation( rootId: String, locationUri: Uri, queryArgs: Bundle?, maxResults: Int, ): Cursor? { val authority = locationUri.authority ?: return null for (userId in mUserIdList) { Log.d(TAG, "BaseFileLoader.queryLocation for $userId at $locationUri") val resolver = userId.getContentResolver(context) try { resolver.acquireUnstableContentProviderClient( authority ).use { client -> if (client == null) { return null } try { val cursor = client.query(locationUri, null, queryArgs, mSignal) ?: return null return RootCursorWrapper(userId, authority, rootId, cursor, maxResults) } catch (e: RemoteException) { Log.d(TAG, "Failed to get cursor for $locationUri", e) } } } catch (e: Exception) { Log.d(TAG, "Failed to get a content provider client for $locationUri", e) } } return null } } src/com/android/documentsui/loaders/FolderLoader.kt 0 → 100644 +79 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.loaders import android.content.Context import android.provider.DocumentsContract import com.android.documentsui.ContentLock import com.android.documentsui.DirectoryResult import com.android.documentsui.LockingContentObserver import com.android.documentsui.base.DocumentInfo import com.android.documentsui.base.FilteringCursorWrapper import com.android.documentsui.base.Lookup import com.android.documentsui.base.RootInfo import com.android.documentsui.base.UserId import com.android.documentsui.sorting.SortModel /** * A specialization of the BaseFileLoader that loads the children of a single folder. To list * a directory you need to provide: * * - The current application context * - A content lock for which a locking content observer is built * - A list of user IDs on behalf of which the search is conducted * - The root info of the listed directory * - The document info of the listed directory * - a lookup from file extension to file type * - The model capable of sorting results */ class FolderLoader( context: Context, userIdList: List<UserId>, mimeTypeLookup: Lookup<String, String>, contentLock: ContentLock, private val mRoot: RootInfo, private val mListedDir: DocumentInfo, private val mOptions: QueryOptions, private val mSortModel: SortModel, ) : BaseFileLoader(context, userIdList, mimeTypeLookup) { // An observer registered on the cursor to force a reload if the cursor reports a change. private val mObserver = LockingContentObserver(contentLock, this::onContentChanged) // Creates a directory result object corresponding to the current parameters of the loader. override fun loadInBackground(): DirectoryResult? { val rejectBeforeTimestamp = mOptions.getRejectBeforeTimestamp() val folderChildrenUri = DocumentsContract.buildChildDocumentsUri( mListedDir.authority, mListedDir.documentId ) var cursor = queryLocation(mRoot.rootId, folderChildrenUri, null, ALL_RESULTS) ?: emptyCursor() val filteredCursor = FilteringCursorWrapper(cursor) filteredCursor.filterHiddenFiles(mOptions.showHidden) 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 result.cursor = sortedCursor return result } } Loading
src/com/android/documentsui/AbstractActionHandler.java +86 −2 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static com.android.documentsui.base.DocumentInfo.getCursorInt; import static com.android.documentsui.base.DocumentInfo.getCursorString; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.flags.Flags.desktopFileHandling; import static com.android.documentsui.flags.Flags.useSearchV2; import android.app.PendingIntent; import android.content.ActivityNotFoundException; Loading @@ -38,6 +39,7 @@ import android.util.Log; import android.util.Pair; import android.view.DragEvent; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager.LoaderCallbacks; Loading @@ -63,6 +65,9 @@ import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.FocusHandler; import com.android.documentsui.files.LauncherActivity; import com.android.documentsui.files.QuickViewIntentBuilder; import com.android.documentsui.loaders.FolderLoader; import com.android.documentsui.loaders.QueryOptions; import com.android.documentsui.loaders.SearchLoader; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.GetRootDocumentTask; import com.android.documentsui.roots.LoadFirstRootTask; Loading @@ -73,10 +78,14 @@ import com.android.documentsui.sorting.SortListFragment; import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.Snackbars; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.function.Consumer; Loading Loading @@ -894,16 +903,28 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { private ExecutorService mExecutorService = null; private static final long MAX_SEARCH_TIME_MS = 3000; private static final int MAX_RESULTS = 500; @NonNull @Override public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { Context context = mActivity; // If document stack is not initialized, i.e. if the root is null, create "Recents" root // with the selected user. if (!mState.stack.isInitialized()) { mState.stack.changeRoot(mActivity.getCurrentRoot()); } if (useSearchV2()) { return onCreateLoaderV2(id, args); } return onCreateLoaderV1(id, args); } private Loader<DirectoryResult> onCreateLoaderV1(int id, Bundle args) { Context context = mActivity; if (mState.stack.isRecents()) { final LockingContentObserver observer = new LockingContentObserver( mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); Loading Loading @@ -980,6 +1001,69 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } } private Loader<DirectoryResult> onCreateLoaderV2(int id, Bundle args) { if (mExecutorService == null) { // TODO(b:388130971): Fine tune the size of the thread pool. mExecutorService = Executors.newFixedThreadPool( GlobalSearchLoader.MAX_OUTSTANDING_TASK); } DocumentStack stack = mState.stack; RootInfo root = stack.getRoot(); List<UserId> userIdList = DocumentsApplication.getUserIdManager(mActivity).getUserIds(); Duration lastModifiedDelta = stack.isRecents() ? Duration.ofMillis(RecentsLoader.REJECT_OLDER_THAN) : null; int maxResults = (root == null || root.isRecents()) ? RecentsLoader.MAX_DOCS_FROM_ROOT : MAX_RESULTS; QueryOptions options = new QueryOptions( maxResults, lastModifiedDelta, Duration.ofMillis(MAX_SEARCH_TIME_MS), mState.showHiddenFiles, mState.acceptMimes); if (stack.isRecents() || mSearchMgr.isSearching()) { Log.d(TAG, "Creating search loader V2"); // For search and recent we create an observer that restart the loader every time // one of the searched content providers reports a change. final LockingContentObserver observer = new LockingContentObserver( mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); Collection<RootInfo> rootList = new ArrayList<>(); if (root == null || root.isRecents()) { // TODO(b:381346575): Pass roots based on user selection. rootList.addAll(mProviders.getMatchingRootsBlocking(mState).stream().filter( r -> r.supportsSearch() && r.authority != null && r.rootId != null).toList()); } else { rootList.add(root); } return new SearchLoader( mActivity, userIdList, mInjector.fileTypeLookup, observer, rootList, mSearchMgr.getCurrentSearch(), options, mState.sortModel, mExecutorService ); } Log.d(TAG, "Creating folder loader V2"); // For folder scan we pass the content lock to the loader so that it can register // an a callback to its internal method that forces a reload of the folder, every // time the content provider reports a change. return new FolderLoader( mActivity, userIdList, mInjector.fileTypeLookup, mContentLock, root, stack.peek(), options, mState.sortModel ); } @Override public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { if (DEBUG) { Loading
src/com/android/documentsui/MultiRootDocumentsLoader.java +1 −1 Original line number Diff line number Diff line Loading @@ -71,7 +71,7 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory // previously returned cursors for filtering/sorting; this currently races // with the UI thread. private static final int MAX_OUTSTANDING_TASK = 4; public static final int MAX_OUTSTANDING_TASK = 4; private static final int MAX_OUTSTANDING_TASK_SVELTE = 2; /** Loading
src/com/android/documentsui/RecentsLoader.java +3 −3 Original line number Diff line number Diff line Loading @@ -37,13 +37,13 @@ public class RecentsLoader extends MultiRootDocumentsLoader { private static final String TAG = "RecentsLoader"; /** Ignore documents older than this age. */ private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; public static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; /** MIME types that should always be excluded from recents. */ /** MIME types that should always be excluded from the Recents view. */ private static final String[] REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR}; /** Maximum documents from a single root. */ private static final int MAX_DOCS_FROM_ROOT = 64; public static final int MAX_DOCS_FROM_ROOT = 64; private final UserId mUserId; Loading
src/com/android/documentsui/loaders/BaseFileLoader.kt 0 → 100644 +208 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.loaders import android.content.Context import android.database.Cursor import android.database.MatrixCursor import android.database.MergeCursor import android.net.Uri import android.os.Bundle import android.os.CancellationSignal import android.os.RemoteException import android.provider.DocumentsContract.Document import android.util.Log import androidx.loader.content.AsyncTaskLoader import com.android.documentsui.DirectoryResult import com.android.documentsui.base.Lookup import com.android.documentsui.base.UserId import com.android.documentsui.roots.RootCursorWrapper const val TAG = "SearchV2" val FILE_ENTRY_COLUMNS = arrayOf( Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SUMMARY, Document.COLUMN_SIZE, Document.COLUMN_ICON, ) fun emptyCursor(): Cursor { return MatrixCursor(FILE_ENTRY_COLUMNS) } /** * Helper function that returns a single, non-null cursor constructed from the given list of * cursors. */ fun toSingleCursor(cursorList: List<Cursor>): Cursor { if (cursorList.isEmpty()) { return emptyCursor() } if (cursorList.size == 1) { return cursorList[0] } return MergeCursor(cursorList.toTypedArray()) } /** * The base class for search and directory loaders. This class implements common functionality * shared by these loaders. The extending classes should implement loadInBackground, which * should call the queryLocation method. */ abstract class BaseFileLoader( context: Context, private val mUserIdList: List<UserId>, protected val mMimeTypeLookup: Lookup<String, String>, ) : AsyncTaskLoader<DirectoryResult>(context) { private var mSignal: CancellationSignal? = null private var mResult: DirectoryResult? = null override fun cancelLoadInBackground() { Log.d(TAG, "BasedFileLoader.cancelLoadInBackground") super.cancelLoadInBackground() synchronized(this) { mSignal?.cancel() } } override fun deliverResult(result: DirectoryResult?) { Log.d(TAG, "BasedFileLoader.deliverResult") if (isReset) { closeResult(result) return } val oldResult: DirectoryResult? = mResult mResult = result if (isStarted) { super.deliverResult(result) } if (oldResult != null && oldResult !== result) { closeResult(oldResult) } } override fun onStartLoading() { Log.d(TAG, "BasedFileLoader.onStartLoading") val isCursorStale: Boolean = checkIfCursorStale(mResult) if (mResult != null && !isCursorStale) { deliverResult(mResult) } if (takeContentChanged() || mResult == null || isCursorStale) { forceLoad() } } override fun onStopLoading() { Log.d(TAG, "BasedFileLoader.onStopLoading") cancelLoad() } override fun onCanceled(result: DirectoryResult?) { Log.d(TAG, "BasedFileLoader.onCanceled") closeResult(result) } override fun onReset() { Log.d(TAG, "BasedFileLoader.onReset") super.onReset() // Ensure the loader is stopped onStopLoading() closeResult(mResult) mResult = null } /** * Quietly closes the result cursor, if results are still available. */ fun closeResult(result: DirectoryResult?) { try { result?.close() } catch (e: Exception) { Log.d(TAG, "Failed to close result", e) } } private fun checkIfCursorStale(result: DirectoryResult?): Boolean { if (result == null) { return true } val cursor = result.cursor ?: return true if (cursor.isClosed) { return true } Log.d(TAG, "Long check of cursor staleness") val count = cursor.count if (!cursor.moveToPosition(-1)) { return true } for (i in 1..count) { if (!cursor.moveToNext()) { return true } } return false } /** * A function that, for the specified location rooted in the root with the given rootId * attempts to obtain a non-null cursor from the content provider client obtained for the * given locationUri. It returns the first non-null cursor, if one can be found, or null, * if it fails to query the given location for all known users. */ fun queryLocation( rootId: String, locationUri: Uri, queryArgs: Bundle?, maxResults: Int, ): Cursor? { val authority = locationUri.authority ?: return null for (userId in mUserIdList) { Log.d(TAG, "BaseFileLoader.queryLocation for $userId at $locationUri") val resolver = userId.getContentResolver(context) try { resolver.acquireUnstableContentProviderClient( authority ).use { client -> if (client == null) { return null } try { val cursor = client.query(locationUri, null, queryArgs, mSignal) ?: return null return RootCursorWrapper(userId, authority, rootId, cursor, maxResults) } catch (e: RemoteException) { Log.d(TAG, "Failed to get cursor for $locationUri", e) } } } catch (e: Exception) { Log.d(TAG, "Failed to get a content provider client for $locationUri", e) } } return null } }
src/com/android/documentsui/loaders/FolderLoader.kt 0 → 100644 +79 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.loaders import android.content.Context import android.provider.DocumentsContract import com.android.documentsui.ContentLock import com.android.documentsui.DirectoryResult import com.android.documentsui.LockingContentObserver import com.android.documentsui.base.DocumentInfo import com.android.documentsui.base.FilteringCursorWrapper import com.android.documentsui.base.Lookup import com.android.documentsui.base.RootInfo import com.android.documentsui.base.UserId import com.android.documentsui.sorting.SortModel /** * A specialization of the BaseFileLoader that loads the children of a single folder. To list * a directory you need to provide: * * - The current application context * - A content lock for which a locking content observer is built * - A list of user IDs on behalf of which the search is conducted * - The root info of the listed directory * - The document info of the listed directory * - a lookup from file extension to file type * - The model capable of sorting results */ class FolderLoader( context: Context, userIdList: List<UserId>, mimeTypeLookup: Lookup<String, String>, contentLock: ContentLock, private val mRoot: RootInfo, private val mListedDir: DocumentInfo, private val mOptions: QueryOptions, private val mSortModel: SortModel, ) : BaseFileLoader(context, userIdList, mimeTypeLookup) { // An observer registered on the cursor to force a reload if the cursor reports a change. private val mObserver = LockingContentObserver(contentLock, this::onContentChanged) // Creates a directory result object corresponding to the current parameters of the loader. override fun loadInBackground(): DirectoryResult? { val rejectBeforeTimestamp = mOptions.getRejectBeforeTimestamp() val folderChildrenUri = DocumentsContract.buildChildDocumentsUri( mListedDir.authority, mListedDir.documentId ) var cursor = queryLocation(mRoot.rootId, folderChildrenUri, null, ALL_RESULTS) ?: emptyCursor() val filteredCursor = FilteringCursorWrapper(cursor) filteredCursor.filterHiddenFiles(mOptions.showHidden) 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 result.cursor = sortedCursor return result } }