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