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