• 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.DocumentsActivity.TAG;
20 import static com.android.documentsui.BaseActivity.State.SORT_ORDER_LAST_MODIFIED;
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.MatrixCursor;
28 import android.database.MergeCursor;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.provider.DocumentsContract;
32 import android.provider.DocumentsContract.Document;
33 import android.provider.DocumentsContract.Root;
34 import android.text.format.DateUtils;
35 import android.util.Log;
36 
37 import com.android.documentsui.BaseActivity.State;
38 import com.android.documentsui.model.RootInfo;
39 import com.android.internal.annotations.GuardedBy;
40 import com.google.android.collect.Maps;
41 import com.google.common.collect.Lists;
42 import com.google.common.util.concurrent.AbstractFuture;
43 
44 import libcore.io.IoUtils;
45 
46 import java.io.Closeable;
47 import java.io.IOException;
48 import java.util.Collection;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.concurrent.CountDownLatch;
52 import java.util.concurrent.ExecutionException;
53 import java.util.concurrent.Semaphore;
54 import java.util.concurrent.TimeUnit;
55 
56 public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
57     private static final boolean LOGD = true;
58 
59     // TODO: clean up cursor ownership so background thread doesn't traverse
60     // previously returned cursors for filtering/sorting; this currently races
61     // with the UI thread.
62 
63     private static final int MAX_OUTSTANDING_RECENTS = 4;
64     private static final int MAX_OUTSTANDING_RECENTS_SVELTE = 2;
65 
66     /**
67      * Time to wait for first pass to complete before returning partial results.
68      */
69     private static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;
70 
71     /** Maximum documents from a single root. */
72     private static final int MAX_DOCS_FROM_ROOT = 64;
73 
74     /** Ignore documents older than this age. */
75     private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS;
76 
77     /** MIME types that should always be excluded from recents. */
78     private static final String[] RECENT_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
79 
80     private final Semaphore mQueryPermits;
81 
82     private final RootsCache mRoots;
83     private final State mState;
84 
85     @GuardedBy("mTasks")
86     private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
87 
88     private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;
89 
90     private CountDownLatch mFirstPassLatch;
91     private volatile boolean mFirstPassDone;
92 
93     private DirectoryResult mResult;
94 
95     // TODO: create better transfer of ownership around cursor to ensure its
96     // closed in all edge cases.
97 
98     public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable {
99         public final String authority;
100         public final String rootId;
101 
102         private Cursor mWithRoot;
103 
RecentTask(String authority, String rootId)104         public RecentTask(String authority, String rootId) {
105             this.authority = authority;
106             this.rootId = rootId;
107         }
108 
109         @Override
run()110         public void run() {
111             if (isCancelled()) return;
112 
113             try {
114                 mQueryPermits.acquire();
115             } catch (InterruptedException e) {
116                 return;
117             }
118 
119             try {
120                 runInternal();
121             } finally {
122                 mQueryPermits.release();
123             }
124         }
125 
runInternal()126         public void runInternal() {
127             ContentProviderClient client = null;
128             try {
129                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
130                         getContext().getContentResolver(), authority);
131 
132                 final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
133                 final Cursor cursor = client.query(
134                         uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
135                 mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
136 
137             } catch (Exception e) {
138                 Log.w(TAG, "Failed to load " + authority + ", " + rootId, e);
139             } finally {
140                 ContentProviderClient.releaseQuietly(client);
141             }
142 
143             set(mWithRoot);
144 
145             mFirstPassLatch.countDown();
146             if (mFirstPassDone) {
147                 onContentChanged();
148             }
149         }
150 
151         @Override
close()152         public void close() throws IOException {
153             IoUtils.closeQuietly(mWithRoot);
154         }
155     }
156 
RecentLoader(Context context, RootsCache roots, State state)157     public RecentLoader(Context context, RootsCache roots, State state) {
158         super(context);
159         mRoots = roots;
160         mState = state;
161 
162         // Keep clients around on high-RAM devices, since we'd be spinning them
163         // up moments later to fetch thumbnails anyway.
164         final ActivityManager am = (ActivityManager) getContext().getSystemService(
165                 Context.ACTIVITY_SERVICE);
166         mQueryPermits = new Semaphore(
167                 am.isLowRamDevice() ? MAX_OUTSTANDING_RECENTS_SVELTE : MAX_OUTSTANDING_RECENTS);
168     }
169 
170     @Override
loadInBackground()171     public DirectoryResult loadInBackground() {
172         synchronized (mTasks) {
173             return loadInBackgroundLocked();
174         }
175     }
176 
loadInBackgroundLocked()177     private DirectoryResult loadInBackgroundLocked() {
178         if (mFirstPassLatch == null) {
179             // First time through we kick off all the recent tasks, and wait
180             // around to see if everyone finishes quickly.
181 
182             final Collection<RootInfo> roots = mRoots.getMatchingRootsBlocking(mState);
183             for (RootInfo root : roots) {
184                 if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
185                     final RecentTask task = new RecentTask(root.authority, root.rootId);
186                     mTasks.put(root, task);
187                 }
188             }
189 
190             mFirstPassLatch = new CountDownLatch(mTasks.size());
191             for (RecentTask task : mTasks.values()) {
192                 ProviderExecutor.forAuthority(task.authority).execute(task);
193             }
194 
195             try {
196                 mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
197                 mFirstPassDone = true;
198             } catch (InterruptedException e) {
199                 throw new RuntimeException(e);
200             }
201         }
202 
203         final long rejectBefore = System.currentTimeMillis() - REJECT_OLDER_THAN;
204 
205         // Collect all finished tasks
206         boolean allDone = true;
207         List<Cursor> cursors = Lists.newArrayList();
208         for (RecentTask task : mTasks.values()) {
209             if (task.isDone()) {
210                 try {
211                     final Cursor cursor = task.get();
212                     if (cursor == null) continue;
213 
214                     final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
215                             cursor, mState.acceptMimes, RECENT_REJECT_MIMES, rejectBefore) {
216                         @Override
217                         public void close() {
218                             // Ignored, since we manage cursor lifecycle internally
219                         }
220                     };
221                     cursors.add(filtered);
222                 } catch (InterruptedException e) {
223                     throw new RuntimeException(e);
224                 } catch (ExecutionException e) {
225                     // We already logged on other side
226                 }
227             } else {
228                 allDone = false;
229             }
230         }
231 
232         if (LOGD) {
233             Log.d(TAG, "Found " + cursors.size() + " of " + mTasks.size() + " recent queries done");
234         }
235 
236         final DirectoryResult result = new DirectoryResult();
237         result.sortOrder = SORT_ORDER_LAST_MODIFIED;
238 
239         // Hint to UI if we're still loading
240         final Bundle extras = new Bundle();
241         if (!allDone) {
242             extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
243         }
244 
245         final Cursor merged;
246         if (cursors.size() > 0) {
247             merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
248         } else {
249             // Return something when nobody is ready
250             merged = new MatrixCursor(new String[0]);
251         }
252 
253         final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder) {
254             @Override
255             public Bundle getExtras() {
256                 return extras;
257             }
258         };
259 
260         result.cursor = sorted;
261 
262         return result;
263     }
264 
265     @Override
cancelLoadInBackground()266     public void cancelLoadInBackground() {
267         super.cancelLoadInBackground();
268     }
269 
270     @Override
deliverResult(DirectoryResult result)271     public void deliverResult(DirectoryResult result) {
272         if (isReset()) {
273             IoUtils.closeQuietly(result);
274             return;
275         }
276         DirectoryResult oldResult = mResult;
277         mResult = result;
278 
279         if (isStarted()) {
280             super.deliverResult(result);
281         }
282 
283         if (oldResult != null && oldResult != result) {
284             IoUtils.closeQuietly(oldResult);
285         }
286     }
287 
288     @Override
onStartLoading()289     protected void onStartLoading() {
290         if (mResult != null) {
291             deliverResult(mResult);
292         }
293         if (takeContentChanged() || mResult == null) {
294             forceLoad();
295         }
296     }
297 
298     @Override
onStopLoading()299     protected void onStopLoading() {
300         cancelLoad();
301     }
302 
303     @Override
onCanceled(DirectoryResult result)304     public void onCanceled(DirectoryResult result) {
305         IoUtils.closeQuietly(result);
306     }
307 
308     @Override
onReset()309     protected void onReset() {
310         super.onReset();
311 
312         // Ensure the loader is stopped
313         onStopLoading();
314 
315         synchronized (mTasks) {
316             for (RecentTask task : mTasks.values()) {
317                 IoUtils.closeQuietly(task);
318             }
319         }
320 
321         IoUtils.closeQuietly(mResult);
322         mResult = null;
323     }
324 }
325