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