• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.base.SharedMinimal.DEBUG;
20 import static com.android.documentsui.base.SharedMinimal.TAG;
21 
22 import android.app.ActivityManager;
23 import android.content.ContentProviderClient;
24 import android.content.Context;
25 import android.database.Cursor;
26 import android.database.CursorWrapper;
27 import android.database.MatrixCursor;
28 import android.database.MergeCursor;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.FileUtils;
32 import android.provider.DocumentsContract;
33 import android.provider.DocumentsContract.Document;
34 import android.util.Log;
35 
36 import androidx.annotation.GuardedBy;
37 import androidx.annotation.NonNull;
38 import androidx.loader.content.AsyncTaskLoader;
39 
40 import com.android.documentsui.base.DocumentInfo;
41 import com.android.documentsui.base.FilteringCursorWrapper;
42 import com.android.documentsui.base.Lookup;
43 import com.android.documentsui.base.RootInfo;
44 import com.android.documentsui.base.State;
45 import com.android.documentsui.roots.ProvidersAccess;
46 import com.android.documentsui.roots.RootCursorWrapper;
47 
48 import com.google.common.util.concurrent.AbstractFuture;
49 
50 import java.io.Closeable;
51 import java.io.IOException;
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.concurrent.CountDownLatch;
58 import java.util.concurrent.ExecutionException;
59 import java.util.concurrent.Executor;
60 import java.util.concurrent.Semaphore;
61 import java.util.concurrent.TimeUnit;
62 
63 /*
64  * The abstract class to query multiple roots from {@link android.provider.DocumentsProvider}
65  * and return the combined result.
66  */
67 public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<DirectoryResult> {
68     // TODO: clean up cursor ownership so background thread doesn't traverse
69     // previously returned cursors for filtering/sorting; this currently races
70     // with the UI thread.
71 
72     private static final int MAX_OUTSTANDING_TASK = 4;
73     private static final int MAX_OUTSTANDING_TASK_SVELTE = 2;
74 
75     /**
76      * Time to wait for first pass to complete before returning partial results.
77      */
78     private static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;
79 
80     protected final State mState;
81 
82     private final Semaphore mQueryPermits;
83     private final ProvidersAccess mProviders;
84     private final Lookup<String, Executor> mExecutors;
85     private final Lookup<String, String> mFileTypeMap;
86 
87     @GuardedBy("mTasks")
88     /** A authority -> QueryTask map */
89     private final Map<String, QueryTask> mTasks = new HashMap<>();
90 
91     private CountDownLatch mFirstPassLatch;
92     private volatile boolean mFirstPassDone;
93 
94     private DirectoryResult mResult;
95 
96     /*
97      * Create the loader to query roots from {@link android.provider.DocumentsProvider}.
98      *
99      * @param context the context
100      * @param providers the providers
101      * @param state current state
102      * @param executors the executors of authorities
103      * @param fileTypeMap the map of mime types and file types.
104      */
MultiRootDocumentsLoader(Context context, ProvidersAccess providers, State state, Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap)105     public MultiRootDocumentsLoader(Context context, ProvidersAccess providers, State state,
106             Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap) {
107 
108         super(context);
109         mProviders = providers;
110         mState = state;
111         mExecutors = executors;
112         mFileTypeMap = fileTypeMap;
113 
114         // Keep clients around on high-RAM devices, since we'd be spinning them
115         // up moments later to fetch thumbnails anyway.
116         final ActivityManager am = (ActivityManager) getContext().getSystemService(
117                 Context.ACTIVITY_SERVICE);
118         mQueryPermits = new Semaphore(
119                 am.isLowRamDevice() ? MAX_OUTSTANDING_TASK_SVELTE : MAX_OUTSTANDING_TASK);
120     }
121 
122     @Override
loadInBackground()123     public DirectoryResult loadInBackground() {
124         synchronized (mTasks) {
125             return loadInBackgroundLocked();
126         }
127     }
128 
loadInBackgroundLocked()129     private DirectoryResult loadInBackgroundLocked() {
130         if (mFirstPassLatch == null) {
131             // First time through we kick off all the recent tasks, and wait
132             // around to see if everyone finishes quickly.
133             Map<String, List<RootInfo>> rootsIndex = indexRoots();
134 
135             for (Map.Entry<String, List<RootInfo>> rootEntry : rootsIndex.entrySet()) {
136                 mTasks.put(rootEntry.getKey(),
137                         getQueryTask(rootEntry.getKey(), rootEntry.getValue()));
138             }
139 
140             mFirstPassLatch = new CountDownLatch(mTasks.size());
141             for (QueryTask task : mTasks.values()) {
142                 mExecutors.lookup(task.authority).execute(task);
143             }
144 
145             try {
146                 mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
147                 mFirstPassDone = true;
148             } catch (InterruptedException e) {
149                 throw new RuntimeException(e);
150             }
151         }
152 
153         final long rejectBefore = getRejectBeforeTime();
154 
155         // Collect all finished tasks
156         boolean allDone = true;
157         int totalQuerySize = 0;
158         List<Cursor> cursors = new ArrayList<>(mTasks.size());
159         for (QueryTask task : mTasks.values()) {
160             if (task.isDone()) {
161                 try {
162                     final Cursor[] taskCursors = task.get();
163                     if (taskCursors == null || taskCursors.length == 0) {
164                         continue;
165                     }
166 
167                     totalQuerySize += taskCursors.length;
168                     for (Cursor cursor : taskCursors) {
169                         if (cursor == null) {
170                             // It's possible given an authority, some roots fail to return a cursor
171                             // after a query.
172                             continue;
173                         }
174                         final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
175                                 cursor, mState.acceptMimes, getRejectMimes(), rejectBefore) {
176                             @Override
177                             public void close() {
178                                 // Ignored, since we manage cursor lifecycle internally
179                             }
180                         };
181                         cursors.add(filtered);
182                     }
183 
184                 } catch (InterruptedException e) {
185                     throw new RuntimeException(e);
186                 } catch (ExecutionException e) {
187                     // We already logged on other side
188                 } catch (Exception e) {
189                     // Catch exceptions thrown when we read the cursor.
190                     Log.e(TAG, "Failed to query documents for authority: " + task.authority
191                             + ". Skip this authority.", e);
192                 }
193             } else {
194                 allDone = false;
195             }
196         }
197 
198         if (DEBUG) {
199             Log.d(TAG,
200                     "Found " + cursors.size() + " of " + totalQuerySize + " queries done");
201         }
202 
203         final DirectoryResult result = new DirectoryResult();
204         result.doc = new DocumentInfo();
205 
206         final Cursor merged;
207         if (cursors.size() > 0) {
208             merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
209         } else {
210             // Return something when nobody is ready
211             merged = new MatrixCursor(new String[0]);
212         }
213 
214         final Cursor sorted;
215         if (isDocumentsMovable()) {
216             sorted = mState.sortModel.sortCursor(merged, mFileTypeMap);
217         } else {
218             final Cursor notMovableMasked = new NotMovableMaskCursor(merged);
219             sorted = mState.sortModel.sortCursor(notMovableMasked, mFileTypeMap);
220         }
221 
222         // Tell the UI if this is an in-progress result. When loading is complete, another update is
223         // sent with EXTRA_LOADING set to false.
224         Bundle extras = new Bundle();
225         extras.putBoolean(DocumentsContract.EXTRA_LOADING, !allDone);
226         sorted.setExtras(extras);
227 
228         result.cursor = sorted;
229 
230         return result;
231     }
232 
233     /**
234      * Returns a map of Authority -> rootInfos.
235      */
indexRoots()236     private Map<String, List<RootInfo>> indexRoots() {
237         final Collection<RootInfo> roots = mProviders.getMatchingRootsBlocking(mState);
238         HashMap<String, List<RootInfo>> rootsIndex = new HashMap<>();
239         for (RootInfo root : roots) {
240             // ignore the root with authority is null. e.g. Recent
241             if (root.authority == null || shouldIgnoreRoot(root)) {
242                 continue;
243             }
244 
245             if (!rootsIndex.containsKey(root.authority)) {
246                 rootsIndex.put(root.authority, new ArrayList<>());
247             }
248             rootsIndex.get(root.authority).add(root);
249         }
250 
251         return rootsIndex;
252     }
253 
getRejectBeforeTime()254     protected long getRejectBeforeTime() {
255         return -1;
256     }
257 
getRejectMimes()258     protected String[] getRejectMimes() {
259         return null;
260     }
261 
shouldIgnoreRoot(RootInfo root)262     protected boolean shouldIgnoreRoot(RootInfo root) {
263         return false;
264     }
265 
isDocumentsMovable()266     protected boolean isDocumentsMovable() {
267         return false;
268     }
269 
getQueryTask(String authority, List<RootInfo> rootInfos)270     protected abstract QueryTask getQueryTask(String authority, List<RootInfo> rootInfos);
271 
272     @Override
deliverResult(DirectoryResult result)273     public void deliverResult(DirectoryResult result) {
274         if (isReset()) {
275             FileUtils.closeQuietly(result);
276             return;
277         }
278         DirectoryResult oldResult = mResult;
279         mResult = result;
280 
281         if (isStarted()) {
282             super.deliverResult(result);
283         }
284 
285         if (oldResult != null && oldResult != result) {
286             FileUtils.closeQuietly(oldResult);
287         }
288     }
289 
290     @Override
onStartLoading()291     protected void onStartLoading() {
292         if (mResult != null) {
293             deliverResult(mResult);
294         }
295         if (takeContentChanged() || mResult == null) {
296             forceLoad();
297         }
298     }
299 
300     @Override
onStopLoading()301     protected void onStopLoading() {
302         cancelLoad();
303     }
304 
305     @Override
onCanceled(DirectoryResult result)306     public void onCanceled(DirectoryResult result) {
307         FileUtils.closeQuietly(result);
308     }
309 
310     @Override
onReset()311     protected void onReset() {
312         super.onReset();
313 
314         // Ensure the loader is stopped
315         onStopLoading();
316 
317         synchronized (mTasks) {
318             for (QueryTask task : mTasks.values()) {
319                 FileUtils.closeQuietly(task);
320             }
321         }
322 
323         FileUtils.closeQuietly(mResult);
324         mResult = null;
325     }
326 
327     // TODO: create better transfer of ownership around cursor to ensure its
328     // closed in all edge cases.
329 
330     private static class NotMovableMaskCursor extends CursorWrapper {
331         private static final int NOT_MOVABLE_MASK =
332                 ~(Document.FLAG_SUPPORTS_DELETE
333                         | Document.FLAG_SUPPORTS_REMOVE
334                         | Document.FLAG_SUPPORTS_MOVE);
335 
NotMovableMaskCursor(Cursor cursor)336         private NotMovableMaskCursor(Cursor cursor) {
337             super(cursor);
338         }
339 
340         @Override
getInt(int index)341         public int getInt(int index) {
342             final int flagIndex = getWrappedCursor().getColumnIndex(Document.COLUMN_FLAGS);
343             final int value = super.getInt(index);
344             return (index == flagIndex) ? (value & NOT_MOVABLE_MASK) : value;
345         }
346     }
347 
348     protected abstract class QueryTask extends AbstractFuture<Cursor[]> implements Runnable,
349             Closeable {
350         public final String authority;
351         public final List<RootInfo> rootInfos;
352 
353         private Cursor[] mCursors;
354         private boolean mIsClosed = false;
355 
QueryTask(String authority, List<RootInfo> rootInfos)356         public QueryTask(String authority, List<RootInfo> rootInfos) {
357             this.authority = authority;
358             this.rootInfos = rootInfos;
359         }
360 
361         @Override
run()362         public void run() {
363             if (isCancelled()) {
364                 return;
365             }
366 
367             try {
368                 mQueryPermits.acquire();
369             } catch (InterruptedException e) {
370                 return;
371             }
372 
373             try {
374                 runInternal();
375             } finally {
376                 mQueryPermits.release();
377             }
378         }
379 
getQueryUri(RootInfo rootInfo)380         protected abstract Uri getQueryUri(RootInfo rootInfo);
381 
generateResultCursor(RootInfo rootInfo, Cursor oriCursor)382         protected abstract RootCursorWrapper generateResultCursor(RootInfo rootInfo,
383                 Cursor oriCursor);
384 
addQueryArgs(@onNull Bundle queryArgs)385         protected void addQueryArgs(@NonNull Bundle queryArgs) {
386         }
387 
runInternal()388         private synchronized void runInternal() {
389             if (mIsClosed) {
390                 return;
391             }
392 
393             ContentProviderClient client = null;
394             try {
395                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
396                         getContext().getContentResolver(), authority);
397 
398                 final int rootInfoCount = rootInfos.size();
399                 final Cursor[] res = new Cursor[rootInfoCount];
400                 mCursors = new Cursor[rootInfoCount];
401 
402                 for (int i = 0; i < rootInfoCount; i++) {
403                     final Uri uri = getQueryUri(rootInfos.get(i));
404                     try {
405                         final Bundle queryArgs = new Bundle();
406                         mState.sortModel.addQuerySortArgs(queryArgs);
407                         addQueryArgs(queryArgs);
408                         res[i] = client.query(uri, null, queryArgs, null);
409                         mCursors[i] = generateResultCursor(rootInfos.get(i), res[i]);
410                     } catch (Exception e) {
411                         Log.w(TAG, "Failed to load " + authority + ", " + rootInfos.get(i).rootId,
412                                 e);
413                     }
414                 }
415 
416             } catch (Exception e) {
417                 Log.w(TAG, "Failed to acquire content resolver for authority: " + authority);
418             } finally {
419                 FileUtils.closeQuietly(client);
420             }
421 
422             set(mCursors);
423 
424             mFirstPassLatch.countDown();
425             if (mFirstPassDone) {
426                 onContentChanged();
427             }
428         }
429 
430         @Override
close()431         public synchronized void close() throws IOException {
432             if (mCursors == null) {
433                 return;
434             }
435 
436             for (Cursor cursor : mCursors) {
437                 FileUtils.closeQuietly(cursor);
438             }
439 
440             mIsClosed = true;
441         }
442     }
443 }
444