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.VERBOSE;
     20 
     21 import android.content.AsyncTaskLoader;
     22 import android.content.ContentProviderClient;
     23 import android.content.ContentResolver;
     24 import android.content.Context;
     25 import android.content.res.Resources;
     26 import android.database.ContentObserver;
     27 import android.database.Cursor;
     28 import android.net.Uri;
     29 import android.os.Bundle;
     30 import android.os.CancellationSignal;
     31 import android.os.Handler;
     32 import android.os.Looper;
     33 import android.os.OperationCanceledException;
     34 import android.os.RemoteException;
     35 import android.provider.DocumentsContract.Document;
     36 import android.util.Log;
     37 
     38 import com.android.documentsui.archives.ArchivesProvider;
     39 import com.android.documentsui.base.DebugFlags;
     40 import com.android.documentsui.base.DocumentInfo;
     41 import com.android.documentsui.base.Features;
     42 import com.android.documentsui.base.FilteringCursorWrapper;
     43 import com.android.documentsui.base.Lookup;
     44 import com.android.documentsui.base.RootInfo;
     45 import com.android.documentsui.roots.RootCursorWrapper;
     46 import com.android.documentsui.sorting.SortModel;
     47 
     48 import libcore.io.IoUtils;
     49 
     50 public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
     51 
     52     private static final String TAG = "DirectoryLoader";
     53 
     54     private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
     55 
     56     private final LockingContentObserver mObserver;
     57     private final RootInfo mRoot;
     58     private final Uri mUri;
     59     private final SortModel mModel;
     60     private final Lookup<String, String> mFileTypeLookup;
     61     private final boolean mSearchMode;
     62 
     63     private DocumentInfo mDoc;
     64     private CancellationSignal mSignal;
     65     private DirectoryResult mResult;
     66 
     67     private Features mFeatures;
     68 
     69     public DirectoryLoader(
     70             Features features,
     71             Context context,
     72             RootInfo root,
     73             DocumentInfo doc,
     74             Uri uri,
     75             SortModel model,
     76             Lookup<String, String> fileTypeLookup,
     77             DirectoryReloadLock lock,
     78             boolean inSearchMode) {
     79 
     80         super(context, ProviderExecutor.forAuthority(root.authority));
     81         mFeatures = features;
     82         mRoot = root;
     83         mUri = uri;
     84         mModel = model;
     85         mDoc = doc;
     86         mFileTypeLookup = fileTypeLookup;
     87         mSearchMode = inSearchMode;
     88         mObserver = new LockingContentObserver(lock, this::onContentChanged);
     89     }
     90 
     91     @Override
     92     public final DirectoryResult loadInBackground() {
     93         synchronized (this) {
     94             if (isLoadInBackgroundCanceled()) {
     95                 throw new OperationCanceledException();
     96             }
     97             mSignal = new CancellationSignal();
     98         }
     99 
    100         final ContentResolver resolver = getContext().getContentResolver();
    101         final String authority = mUri.getAuthority();
    102 
    103         final DirectoryResult result = new DirectoryResult();
    104         result.doc = mDoc;
    105 
    106         ContentProviderClient client = null;
    107         Cursor cursor;
    108         try {
    109             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
    110             if (mDoc.isInArchive()) {
    111                 ArchivesProvider.acquireArchive(client, mUri);
    112             }
    113             result.client = client;
    114 
    115             Resources resources = getContext().getResources();
    116             if (mFeatures.isContentPagingEnabled()) {
    117                 Bundle queryArgs = new Bundle();
    118                 mModel.addQuerySortArgs(queryArgs);
    119 
    120                 // TODO: At some point we don't want forced flags to override real paging...
    121                 // and that point is when we have real paging.
    122                 DebugFlags.addForcedPagingArgs(queryArgs);
    123 
    124                 cursor = client.query(mUri, null, queryArgs, mSignal);
    125             } else {
    126                 cursor = client.query(
    127                         mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal);
    128             }
    129 
    130             if (cursor == null) {
    131                 throw new RemoteException("Provider returned null");
    132             }
    133 
    134             cursor.registerContentObserver(mObserver);
    135 
    136             cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1);
    137 
    138             if (mSearchMode && !mFeatures.isFoldersInSearchResultsEnabled()) {
    139                 // There is no findDocumentPath API. Enable filtering on folders in search mode.
    140                 cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
    141             }
    142 
    143             // TODO: When API tweaks have landed, use ContentResolver.EXTRA_HONORED_ARGS
    144             // instead of checking directly for ContentResolver.QUERY_ARG_SORT_COLUMNS (won't work)
    145             if (mFeatures.isContentPagingEnabled()
    146                         && cursor.getExtras().containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) {
    147                 if (VERBOSE) Log.d(TAG, "Skipping sort of pre-sorted cursor. Booya!");
    148             } else {
    149                 cursor = mModel.sortCursor(cursor, mFileTypeLookup);
    150             }
    151             result.cursor = cursor;
    152         } catch (Exception e) {
    153             Log.w(TAG, "Failed to query", e);
    154             result.exception = e;
    155         } finally {
    156             synchronized (this) {
    157                 mSignal = null;
    158             }
    159             // TODO: Remove this call.
    160             ContentProviderClient.releaseQuietly(client);
    161         }
    162 
    163         return result;
    164     }
    165 
    166     @Override
    167     public void cancelLoadInBackground() {
    168         super.cancelLoadInBackground();
    169 
    170         synchronized (this) {
    171             if (mSignal != null) {
    172                 mSignal.cancel();
    173             }
    174         }
    175     }
    176 
    177     @Override
    178     public void deliverResult(DirectoryResult result) {
    179         if (isReset()) {
    180             IoUtils.closeQuietly(result);
    181             return;
    182         }
    183         DirectoryResult oldResult = mResult;
    184         mResult = result;
    185 
    186         if (isStarted()) {
    187             super.deliverResult(result);
    188         }
    189 
    190         if (oldResult != null && oldResult != result) {
    191             IoUtils.closeQuietly(oldResult);
    192         }
    193     }
    194 
    195     @Override
    196     protected void onStartLoading() {
    197         if (mResult != null) {
    198             deliverResult(mResult);
    199         }
    200         if (takeContentChanged() || mResult == null) {
    201             forceLoad();
    202         }
    203     }
    204 
    205     @Override
    206     protected void onStopLoading() {
    207         cancelLoad();
    208     }
    209 
    210     @Override
    211     public void onCanceled(DirectoryResult result) {
    212         IoUtils.closeQuietly(result);
    213     }
    214 
    215     @Override
    216     protected void onReset() {
    217         super.onReset();
    218 
    219         // Ensure the loader is stopped
    220         onStopLoading();
    221 
    222         IoUtils.closeQuietly(mResult);
    223         mResult = null;
    224 
    225         getContext().getContentResolver().unregisterContentObserver(mObserver);
    226     }
    227 
    228     private static final class LockingContentObserver extends ContentObserver {
    229         private final DirectoryReloadLock mLock;
    230         private final Runnable mContentChangedCallback;
    231 
    232         public LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback) {
    233             super(new Handler(Looper.getMainLooper()));
    234             mLock = lock;
    235             mContentChangedCallback = contentChangedCallback;
    236         }
    237 
    238         @Override
    239         public boolean deliverSelfNotifications() {
    240             return true;
    241         }
    242 
    243         @Override
    244         public void onChange(boolean selfChange) {
    245             mLock.tryUpdate(mContentChangedCallback);
    246         }
    247     }
    248 }
    249