• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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