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