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