blob: 3659c6e11e7a1bd667446ba72575a69c438cd832 [file] [log] [blame]
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001/*
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
17package com.android.documentsui;
18
19import static com.android.documentsui.DocumentsActivity.TAG;
Jeff Sharkeya4d1f222013-09-07 14:45:03 -070020import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -070021
22import android.content.AsyncTaskLoader;
23import android.content.ContentProviderClient;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.database.Cursor;
27import android.database.MergeCursor;
28import android.net.Uri;
29import android.provider.DocumentsContract;
Jeff Sharkey9656a532013-09-13 13:42:19 -070030import android.provider.DocumentsContract.Document;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -070031import android.provider.DocumentsContract.Root;
32import android.util.Log;
33
34import com.android.documentsui.DocumentsActivity.State;
35import com.android.documentsui.model.RootInfo;
36import com.google.android.collect.Maps;
37import com.google.common.collect.Lists;
38import com.google.common.util.concurrent.AbstractFuture;
39
40import libcore.io.IoUtils;
41
42import java.io.Closeable;
43import java.io.IOException;
Jeff Sharkey8b997042013-09-19 15:25:56 -070044import java.util.Collection;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -070045import java.util.HashMap;
46import java.util.List;
47import java.util.concurrent.CountDownLatch;
48import java.util.concurrent.ExecutionException;
49import java.util.concurrent.ExecutorService;
50import java.util.concurrent.LinkedBlockingQueue;
51import java.util.concurrent.ThreadPoolExecutor;
52import java.util.concurrent.TimeUnit;
53
54public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
55
56 public static final int MAX_OUTSTANDING_RECENTS = 2;
57
58 /**
59 * Time to wait for first pass to complete before returning partial results.
60 */
61 public static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;
62
63 /**
64 * Maximum documents from a single root.
65 */
66 public static final int MAX_DOCS_FROM_ROOT = 24;
67
68 private static final ExecutorService sExecutor = buildExecutor();
69
70 /**
71 * Create a bounded thread pool for fetching recents; it creates threads as
72 * needed (up to maximum) and reclaims them when finished.
73 */
74 private static ExecutorService buildExecutor() {
75 // Create a bounded thread pool for fetching recents; it creates
76 // threads as needed (up to maximum) and reclaims them when finished.
77 final ThreadPoolExecutor executor = new ThreadPoolExecutor(
78 MAX_OUTSTANDING_RECENTS, MAX_OUTSTANDING_RECENTS, 10, TimeUnit.SECONDS,
79 new LinkedBlockingQueue<Runnable>());
80 executor.allowCoreThreadTimeOut(true);
81 return executor;
82 }
83
Jeff Sharkey8b997042013-09-19 15:25:56 -070084 private final RootsCache mRoots;
85 private final State mState;
Jeff Sharkey1c903cc2013-09-02 17:19:40 -070086
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -070087 private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
88
89 private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;
90
91 private CountDownLatch mFirstPassLatch;
92 private volatile boolean mFirstPassDone;
93
94 private DirectoryResult mResult;
95
96 // TODO: create better transfer of ownership around cursor to ensure its
97 // closed in all edge cases.
98
99 public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable {
100 public final String authority;
101 public final String rootId;
102
103 private Cursor mWithRoot;
104
105 public RecentTask(String authority, String rootId) {
106 this.authority = authority;
107 this.rootId = rootId;
108 }
109
110 @Override
111 public void run() {
112 if (isCancelled()) return;
113
114 final ContentResolver resolver = getContext().getContentResolver();
115 final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
116 authority);
117 try {
118 final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
119 final Cursor cursor = client.query(
120 uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
121 mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
122 set(mWithRoot);
123
124 mFirstPassLatch.countDown();
125 if (mFirstPassDone) {
126 onContentChanged();
127 }
128
129 } catch (Exception e) {
130 setException(e);
131 } finally {
132 ContentProviderClient.closeQuietly(client);
133 }
134 }
135
136 @Override
137 public void close() throws IOException {
138 IoUtils.closeQuietly(mWithRoot);
139 }
140 }
141
Jeff Sharkey8b997042013-09-19 15:25:56 -0700142 public RecentLoader(Context context, RootsCache roots, State state) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700143 super(context);
Jeff Sharkey1c903cc2013-09-02 17:19:40 -0700144 mRoots = roots;
Jeff Sharkey8b997042013-09-19 15:25:56 -0700145 mState = state;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700146 }
147
148 @Override
149 public DirectoryResult loadInBackground() {
150 if (mFirstPassLatch == null) {
151 // First time through we kick off all the recent tasks, and wait
152 // around to see if everyone finishes quickly.
153
Jeff Sharkey8b997042013-09-19 15:25:56 -0700154 final Collection<RootInfo> roots = mRoots.getMatchingRootsBlocking(mState);
155 for (RootInfo root : roots) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700156 if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
157 final RecentTask task = new RecentTask(root.authority, root.rootId);
158 mTasks.put(root, task);
159 }
160 }
161
162 mFirstPassLatch = new CountDownLatch(mTasks.size());
163 for (RecentTask task : mTasks.values()) {
164 sExecutor.execute(task);
165 }
166
167 try {
168 mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
169 mFirstPassDone = true;
170 } catch (InterruptedException e) {
171 throw new RuntimeException(e);
172 }
173 }
174
175 // Collect all finished tasks
176 List<Cursor> cursors = Lists.newArrayList();
177 for (RecentTask task : mTasks.values()) {
178 if (task.isDone()) {
179 try {
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700180 final Cursor cursor = task.get();
181 final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
Jeff Sharkey8b997042013-09-19 15:25:56 -0700182 cursor, mState.acceptMimes, new String[] { Document.MIME_TYPE_DIR }) {
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700183 @Override
184 public void close() {
185 // Ignored, since we manage cursor lifecycle internally
186 }
187 };
188 cursors.add(filtered);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700189 } catch (InterruptedException e) {
190 throw new RuntimeException(e);
191 } catch (ExecutionException e) {
192 Log.w(TAG, "Failed to load " + task.authority + ", " + task.rootId, e);
193 }
194 }
195 }
196
197 final DirectoryResult result = new DirectoryResult();
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700198 result.sortOrder = SORT_ORDER_LAST_MODIFIED;
199
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700200 if (cursors.size() > 0) {
201 final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700202 final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700203 result.cursor = sorted;
204 }
205 return result;
206 }
207
208 @Override
209 public void cancelLoadInBackground() {
210 super.cancelLoadInBackground();
211 }
212
213 @Override
214 public void deliverResult(DirectoryResult result) {
215 if (isReset()) {
216 IoUtils.closeQuietly(result);
217 return;
218 }
219 DirectoryResult oldResult = mResult;
220 mResult = result;
221
222 if (isStarted()) {
223 super.deliverResult(result);
224 }
225
226 if (oldResult != null && oldResult != result) {
227 IoUtils.closeQuietly(oldResult);
228 }
229 }
230
231 @Override
232 protected void onStartLoading() {
233 if (mResult != null) {
234 deliverResult(mResult);
235 }
236 if (takeContentChanged() || mResult == null) {
237 forceLoad();
238 }
239 }
240
241 @Override
242 protected void onStopLoading() {
243 cancelLoad();
244 }
245
246 @Override
247 public void onCanceled(DirectoryResult result) {
248 IoUtils.closeQuietly(result);
249 }
250
251 @Override
252 protected void onReset() {
253 super.onReset();
254
255 // Ensure the loader is stopped
256 onStopLoading();
257
258 for (RecentTask task : mTasks.values()) {
259 IoUtils.closeQuietly(task);
260 }
261
262 IoUtils.closeQuietly(mResult);
263 mResult = null;
264 }
265}