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