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