1 /* 2 * Copyright (C) 2024 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 package com.android.documentsui.loaders 17 18 import android.content.Context 19 import android.database.Cursor 20 import android.net.Uri 21 import android.os.Bundle 22 import android.provider.DocumentsContract 23 import android.provider.DocumentsContract.Document 24 import android.text.TextUtils 25 import android.util.Log 26 import com.android.documentsui.DirectoryResult 27 import com.android.documentsui.LockingContentObserver 28 import com.android.documentsui.base.DocumentInfo 29 import com.android.documentsui.base.FilteringCursorWrapper 30 import com.android.documentsui.base.Lookup 31 import com.android.documentsui.base.RootInfo 32 import com.android.documentsui.base.UserId 33 import com.android.documentsui.sorting.SortModel 34 import com.google.common.util.concurrent.AbstractFuture 35 import java.io.Closeable 36 import java.util.concurrent.CountDownLatch 37 import java.util.concurrent.ExecutorService 38 import java.util.concurrent.TimeUnit 39 import kotlin.time.measureTime 40 41 /** 42 * A specialization of the BaseFileLoader that searches the set of specified roots. To search 43 * the roots you must provider: 44 * 45 * - The current application context 46 * - A content lock for which a locking content observer is built 47 * - A list of user IDs, on whose behalf we query content provider clients. 48 * - A list of RootInfo objects representing searched roots 49 * - A query used to search for matching files. 50 * - Query options such as maximum number of results, last modified time delta, etc. 51 * - a lookup from file extension to file type 52 * - The model capable of sorting results 53 * - An executor for running searches across multiple roots in parallel 54 * 55 * SearchLoader requires that either a query is not null and not empty or that QueryOptions 56 * specify a last modified time restriction. This is to prevent searching for every file 57 * across every specified root. 58 */ 59 class SearchLoader( 60 context: Context, 61 userIdList: List<UserId>, 62 mimeTypeLookup: Lookup<String, String>, 63 private val mObserver: LockingContentObserver, 64 private val mRootList: Collection<RootInfo>, 65 private val mQuery: String?, 66 private val mOptions: QueryOptions, 67 private val mSortModel: SortModel, 68 private val mExecutorService: ExecutorService, 69 ) : BaseFileLoader(context, userIdList, mimeTypeLookup) { 70 71 /** 72 * Helper class that runs query on a single user for the given parameter. This class implements 73 * an abstract future so that if the task is completed, we can retrieve the cursor via the get 74 * method. 75 */ 76 inner class SearchTask( 77 private val mRootId: String, 78 private val mSearchUri: Uri, 79 private val mQueryArgs: Bundle, 80 private val mLatch: CountDownLatch, 81 ) : Closeable, Runnable, AbstractFuture<Cursor>() { 82 private var mCursor: Cursor? = null 83 val cursor: Cursor? get() = mCursor 84 val taskId: String get() = mSearchUri.toString() 85 closenull86 override fun close() { 87 mCursor = null 88 } 89 runnull90 override fun run() { 91 val queryDuration = measureTime { 92 try { 93 mCursor = queryLocation(mRootId, mSearchUri, mQueryArgs, mOptions.maxResults) 94 set(mCursor) 95 } finally { 96 mLatch.countDown() 97 } 98 } 99 Log.d(TAG, "Query on $mSearchUri took $queryDuration") 100 } 101 } 102 103 @Volatile 104 private var mSearchTaskList: List<SearchTask> = listOf() 105 106 // Creates a directory result object corresponding to the current parameters of the loader. loadInBackgroundnull107 override fun loadInBackground(): DirectoryResult? { 108 val result = DirectoryResult() 109 // TODO(b:378590632): If root list has one root use it to construct result.doc 110 result.doc = DocumentInfo() 111 result.cursor = emptyCursor() 112 113 val searchedRoots = mRootList 114 val countDownLatch = CountDownLatch(searchedRoots.size) 115 val rejectBeforeTimestamp = mOptions.getRejectBeforeTimestamp() 116 117 // Step 1: Build a list of search tasks. 118 val searchTaskList = 119 createSearchTaskList(rejectBeforeTimestamp, countDownLatch, mRootList) 120 Log.d(TAG, "${searchTaskList.size} tasks have been created") 121 122 // Check if we are cancelled; if not copy the task list. 123 if (isLoadInBackgroundCanceled) { 124 return result 125 } 126 mSearchTaskList = searchTaskList 127 128 // Step 2: Enqueue tasks and wait for them to complete or time out. 129 for (task in mSearchTaskList) { 130 mExecutorService.execute(task) 131 } 132 Log.d(TAG, "${mSearchTaskList.size} tasks have been enqueued") 133 134 // Step 3: Wait for the results. 135 try { 136 if (mOptions.isQueryTimeUnlimited()) { 137 Log.d(TAG, "Waiting for results with no time limit") 138 countDownLatch.await() 139 } else { 140 Log.d(TAG, "Waiting ${mOptions.maxQueryTime!!.toMillis()}ms for results") 141 countDownLatch.await( 142 mOptions.maxQueryTime.toMillis(), 143 TimeUnit.MILLISECONDS 144 ) 145 } 146 Log.d(TAG, "Waiting for results is done") 147 } catch (e: InterruptedException) { 148 Log.d(TAG, "Failed to complete all searches within ${mOptions.maxQueryTime}") 149 // TODO(b:388336095): Record a metrics indicating incomplete search. 150 throw RuntimeException(e) 151 } 152 153 // Step 4: Collect cursors from done tasks. 154 val cursorList = mutableListOf<Cursor>() 155 for (task in mSearchTaskList) { 156 Log.d(TAG, "Processing task ${task.taskId}") 157 if (isLoadInBackgroundCanceled) { 158 break 159 } 160 // TODO(b:388336095): Record a metric for each done and not done task. 161 val cursor = task.cursor 162 if (task.isDone && cursor != null) { 163 // TODO(b:388336095): Record a metric for null and not null cursor. 164 Log.d(TAG, "Task ${task.taskId} has ${cursor.count} results") 165 cursorList.add(cursor) 166 } 167 } 168 Log.d(TAG, "Search complete with ${cursorList.size} cursors collected") 169 170 // Step 5: Assign the cursor, after adding filtering and sorting, to the results. 171 val mergedCursor = toSingleCursor(cursorList) 172 mergedCursor.registerContentObserver(mObserver) 173 val filteringCursor = FilteringCursorWrapper(mergedCursor) 174 filteringCursor.filterHiddenFiles(mOptions.showHidden) 175 filteringCursor.filterMimes( 176 mOptions.acceptableMimeTypes, 177 if (TextUtils.isEmpty(mQuery)) arrayOf<String>(Document.MIME_TYPE_DIR) else null 178 ) 179 if (rejectBeforeTimestamp > 0L) { 180 filteringCursor.filterLastModified(rejectBeforeTimestamp) 181 } 182 result.cursor = mSortModel.sortCursor(filteringCursor, mMimeTypeLookup) 183 184 // TODO(b:388336095): Record the total time it took to complete search. 185 return result 186 } 187 createContentProviderQuerynull188 private fun createContentProviderQuery(root: RootInfo) = 189 if (TextUtils.isEmpty(mQuery) && mOptions.otherQueryArgs.isEmpty) { 190 // NOTE: recent document URI does not respect query-arg-mime-types restrictions. Thus 191 // we only create the recents URI if both the query and other args are empty. 192 DocumentsContract.buildRecentDocumentsUri( 193 root.authority, 194 root.rootId 195 ) 196 } else { 197 // NOTE: We pass empty query, as the name matching query is placed in queryArgs. 198 DocumentsContract.buildSearchDocumentsUri( 199 root.authority, 200 root.rootId, 201 "" 202 ) 203 } 204 createQueryArgsnull205 private fun createQueryArgs(rejectBeforeTimestamp: Long): Bundle { 206 val queryArgs = Bundle() 207 mSortModel.addQuerySortArgs(queryArgs) 208 if (rejectBeforeTimestamp > 0L) { 209 queryArgs.putLong( 210 DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, 211 rejectBeforeTimestamp 212 ) 213 } 214 if (!TextUtils.isEmpty(mQuery)) { 215 queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, mQuery) 216 } 217 queryArgs.putAll(mOptions.otherQueryArgs) 218 return queryArgs 219 } 220 221 /** 222 * Helper function that creates a list of search tasks for the given countdown latch. 223 */ createSearchTaskListnull224 private fun createSearchTaskList( 225 rejectBeforeTimestamp: Long, 226 countDownLatch: CountDownLatch, 227 rootList: Collection<RootInfo> 228 ): List<SearchTask> { 229 val searchTaskList = mutableListOf<SearchTask>() 230 for (root in rootList) { 231 if (isLoadInBackgroundCanceled) { 232 break 233 } 234 val rootSearchUri = createContentProviderQuery(root) 235 // TODO(b:385789236): Correctly pass sort order information. 236 val queryArgs = createQueryArgs(rejectBeforeTimestamp) 237 mSortModel.addQuerySortArgs(queryArgs) 238 Log.d(TAG, "Query $rootSearchUri and queryArgs $queryArgs") 239 val task = SearchTask( 240 root.rootId, 241 rootSearchUri, 242 queryArgs, 243 countDownLatch 244 ) 245 searchTaskList.add(task) 246 } 247 return searchTaskList 248 } 249 onResetnull250 override fun onReset() { 251 for (task in mSearchTaskList) { 252 task.close() 253 } 254 Log.d(TAG, "Resetting search loader; search task list emptied.") 255 super.onReset() 256 } 257 } 258