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.Shared.DEBUG; 20 import static com.android.documentsui.base.Shared.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 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 121 public DirectoryResult loadInBackground() { 122 synchronized (mTasks) { 123 return loadInBackgroundLocked(); 124 } 125 } 126 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 */ 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 243 public void cancelLoadInBackground() { 244 super.cancelLoadInBackground(); 245 } 246 247 @Override 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 266 protected void onStartLoading() { 267 if (mResult != null) { 268 deliverResult(mResult); 269 } 270 if (takeContentChanged() || mResult == null) { 271 forceLoad(); 272 } 273 } 274 275 @Override 276 protected void onStopLoading() { 277 cancelLoad(); 278 } 279 280 @Override 281 public void onCanceled(DirectoryResult result) { 282 IoUtils.closeQuietly(result); 283 } 284 285 @Override 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 312 public RecentsTask(String authority, List<String> rootIds) { 313 this.authority = authority; 314 this.rootIds = rootIds; 315 } 316 317 @Override 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 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 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 399 private NotMovableMaskCursor(Cursor cursor) { 400 super(cursor); 401 } 402 403 @Override 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