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