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