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 DirectoryLoader( Features features, Context context, RootInfo root, DocumentInfo doc, Uri uri, SortModel model, Lookup<String, String> fileTypeLookup, DirectoryReloadLock lock, boolean inSearchMode)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 loadInBackground()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 cancelLoadInBackground()167 public void cancelLoadInBackground() { 168 super.cancelLoadInBackground(); 169 170 synchronized (this) { 171 if (mSignal != null) { 172 mSignal.cancel(); 173 } 174 } 175 } 176 177 @Override deliverResult(DirectoryResult result)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 onStartLoading()196 protected void onStartLoading() { 197 if (mResult != null) { 198 deliverResult(mResult); 199 } 200 if (takeContentChanged() || mResult == null) { 201 forceLoad(); 202 } 203 } 204 205 @Override onStopLoading()206 protected void onStopLoading() { 207 cancelLoad(); 208 } 209 210 @Override onCanceled(DirectoryResult result)211 public void onCanceled(DirectoryResult result) { 212 IoUtils.closeQuietly(result); 213 } 214 215 @Override onReset()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 LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback)232 public LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback) { 233 super(new Handler(Looper.getMainLooper())); 234 mLock = lock; 235 mContentChangedCallback = contentChangedCallback; 236 } 237 238 @Override deliverSelfNotifications()239 public boolean deliverSelfNotifications() { 240 return true; 241 } 242 243 @Override onChange(boolean selfChange)244 public void onChange(boolean selfChange) { 245 mLock.tryUpdate(mContentChangedCallback); 246 } 247 } 248 } 249