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