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.SharedMinimal.VERBOSE; 20 21 import android.content.ContentProviderClient; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.database.MergeCursor; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.CancellationSignal; 29 import android.os.FileUtils; 30 import android.os.OperationCanceledException; 31 import android.os.RemoteException; 32 import android.provider.DocumentsContract; 33 import android.provider.DocumentsContract.Document; 34 import android.util.Log; 35 36 import androidx.annotation.Nullable; 37 import androidx.loader.content.AsyncTaskLoader; 38 39 import com.android.documentsui.archives.ArchivesProvider; 40 import com.android.documentsui.base.DebugFlags; 41 import com.android.documentsui.base.DocumentInfo; 42 import com.android.documentsui.base.Features; 43 import com.android.documentsui.base.FilteringCursorWrapper; 44 import com.android.documentsui.base.Lookup; 45 import com.android.documentsui.base.MimeTypes; 46 import com.android.documentsui.base.RootInfo; 47 import com.android.documentsui.base.State; 48 import com.android.documentsui.base.UserId; 49 import com.android.documentsui.roots.RootCursorWrapper; 50 import com.android.documentsui.sorting.SortModel; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.concurrent.Executor; 55 56 public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { 57 58 private static final String TAG = "DirectoryLoader"; 59 private static final String[] SEARCH_REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR}; 60 private static final String[] PHOTO_PICKING_ACCEPT_MIMES = new String[] 61 {Document.MIME_TYPE_DIR, MimeTypes.IMAGE_MIME}; 62 63 private final LockingContentObserver mObserver; 64 private final RootInfo mRoot; 65 private final State mState; 66 private final Uri mUri; 67 private final SortModel mModel; 68 private final Lookup<String, String> mFileTypeLookup; 69 private final boolean mSearchMode; 70 private final Bundle mQueryArgs; 71 private final boolean mPhotoPicking; 72 73 @Nullable 74 private DocumentInfo mDoc; 75 private CancellationSignal mSignal; 76 private DirectoryResult mResult; 77 78 private Features mFeatures; 79 DirectoryLoader( Features features, Context context, State state, Uri uri, Lookup<String, String> fileTypeLookup, ContentLock lock, Bundle queryArgs)80 public DirectoryLoader( 81 Features features, 82 Context context, 83 State state, 84 Uri uri, 85 Lookup<String, String> fileTypeLookup, 86 ContentLock lock, 87 Bundle queryArgs) { 88 89 super(context); 90 mFeatures = features; 91 mState = state; 92 mRoot = state.stack.getRoot(); 93 mUri = uri; 94 mModel = state.sortModel; 95 mDoc = state.stack.peek(); 96 mFileTypeLookup = fileTypeLookup; 97 mSearchMode = queryArgs != null; 98 mQueryArgs = queryArgs; 99 mObserver = new LockingContentObserver(lock, this::onContentChanged); 100 mPhotoPicking = state.isPhotoPicking(); 101 } 102 103 @Override getExecutor()104 protected Executor getExecutor() { 105 return ProviderExecutor.forAuthority(mRoot.authority); 106 } 107 108 @Override loadInBackground()109 public final DirectoryResult loadInBackground() { 110 synchronized (this) { 111 if (isLoadInBackgroundCanceled()) { 112 throw new OperationCanceledException(); 113 } 114 mSignal = new CancellationSignal(); 115 } 116 117 final String authority = mUri.getAuthority(); 118 119 final DirectoryResult result = new DirectoryResult(); 120 result.doc = mDoc; 121 122 ContentProviderClient client = null; 123 Cursor cursor; 124 try { 125 final Bundle queryArgs = new Bundle(); 126 mModel.addQuerySortArgs(queryArgs); 127 128 final List<UserId> userIds = new ArrayList<>(); 129 if (mSearchMode) { 130 queryArgs.putAll(mQueryArgs); 131 if (shouldSearchAcrossProfile()) { 132 for (UserId userId : DocumentsApplication.getUserIdManager( 133 getContext()).getUserIds()) { 134 if (mState.canInteractWith(userId)) { 135 userIds.add(userId); 136 } 137 } 138 } 139 } 140 if (userIds.isEmpty()) { 141 userIds.add(mRoot.userId); 142 } 143 144 if (userIds.size() == 1) { 145 if (!mState.canInteractWith(mRoot.userId)) { 146 result.exception = new CrossProfileNoPermissionException(); 147 return result; 148 } else if (mRoot.userId.isQuietModeEnabled(getContext())) { 149 result.exception = new CrossProfileQuietModeException(mRoot.userId); 150 return result; 151 } else if (mDoc == null) { 152 // TODO (b/35996595): Consider plumbing through the actual exception, though it 153 // might not be very useful (always pointing to 154 // DatabaseUtils#readExceptionFromParcel()). 155 result.exception = new IllegalStateException("Failed to load root document."); 156 return result; 157 } 158 } 159 160 if (mDoc != null && mDoc.isInArchive()) { 161 final ContentResolver resolver = mRoot.userId.getContentResolver(getContext()); 162 client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); 163 ArchivesProvider.acquireArchive(client, mUri); 164 result.client = client; 165 } 166 167 if (mFeatures.isContentPagingEnabled()) { 168 // TODO: At some point we don't want forced flags to override real paging... 169 // and that point is when we have real paging. 170 DebugFlags.addForcedPagingArgs(queryArgs); 171 } 172 173 cursor = queryOnUsers(userIds, authority, queryArgs); 174 175 if (cursor == null) { 176 throw new RemoteException("Provider returned null"); 177 } 178 cursor.registerContentObserver(mObserver); 179 180 FilteringCursorWrapper filteringCursor = new FilteringCursorWrapper(cursor); 181 filteringCursor.filterHiddenFiles(mState.showHiddenFiles); 182 if (mSearchMode && !mFeatures.isFoldersInSearchResultsEnabled()) { 183 // There is no findDocumentPath API. Enable filtering on folders in search mode. 184 filteringCursor.filterMimes(/* acceptMimes= */ null, SEARCH_REJECT_MIMES); 185 } 186 if (mPhotoPicking) { 187 filteringCursor.filterMimes(PHOTO_PICKING_ACCEPT_MIMES, /* rejectMimes= */ null); 188 } 189 cursor = filteringCursor; 190 191 // TODO: When API tweaks have landed, use ContentResolver.EXTRA_HONORED_ARGS 192 // instead of checking directly for ContentResolver.QUERY_ARG_SORT_COLUMNS (won't work) 193 if (mFeatures.isContentPagingEnabled() 194 && cursor.getExtras().containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { 195 if (VERBOSE) Log.d(TAG, "Skipping sort of pre-sorted cursor. Booya!"); 196 } else { 197 cursor = mModel.sortCursor(cursor, mFileTypeLookup); 198 } 199 result.setCursor(cursor); 200 } catch (Exception e) { 201 Log.w(TAG, "Failed to query", e); 202 result.exception = e; 203 FileUtils.closeQuietly(client); 204 } finally { 205 synchronized (this) { 206 mSignal = null; 207 } 208 } 209 210 return result; 211 } 212 shouldSearchAcrossProfile()213 private boolean shouldSearchAcrossProfile() { 214 return mState.supportsCrossProfile() 215 && mRoot.supportsCrossProfile() 216 && mQueryArgs.containsKey(DocumentsContract.QUERY_ARG_DISPLAY_NAME); 217 } 218 219 @Nullable queryOnUsers(List<UserId> userIds, String authority, Bundle queryArgs)220 private Cursor queryOnUsers(List<UserId> userIds, String authority, Bundle queryArgs) 221 throws RemoteException { 222 final List<Cursor> cursors = new ArrayList<>(userIds.size()); 223 for (UserId userId : userIds) { 224 try (ContentProviderClient userClient = 225 DocumentsApplication.acquireUnstableProviderOrThrow( 226 userId.getContentResolver(getContext()), authority)) { 227 Cursor c = userClient.query(mUri, /* projection= */null, queryArgs, mSignal); 228 if (c != null) { 229 cursors.add(new RootCursorWrapper(userId, mUri.getAuthority(), mRoot.rootId, 230 c, /* maxCount= */-1)); 231 } 232 } catch (RemoteException e) { 233 Log.d(TAG, "Failed to query for user " + userId, e); 234 // Searching on other profile may not succeed because profile may be in quiet mode. 235 if (UserId.CURRENT_USER.equals(userId)) { 236 throw e; 237 } 238 } 239 } 240 int size = cursors.size(); 241 switch (size) { 242 case 0: 243 return null; 244 case 1: 245 return cursors.get(0); 246 default: 247 return new MergeCursor(cursors.toArray(new Cursor[size])); 248 } 249 } 250 251 @Override cancelLoadInBackground()252 public void cancelLoadInBackground() { 253 super.cancelLoadInBackground(); 254 255 synchronized (this) { 256 if (mSignal != null) { 257 mSignal.cancel(); 258 } 259 } 260 } 261 262 @Override deliverResult(DirectoryResult result)263 public void deliverResult(DirectoryResult result) { 264 if (isReset()) { 265 FileUtils.closeQuietly(result); 266 return; 267 } 268 DirectoryResult oldResult = mResult; 269 mResult = result; 270 271 if (isStarted()) { 272 super.deliverResult(result); 273 } 274 275 if (oldResult != null && oldResult != result) { 276 FileUtils.closeQuietly(oldResult); 277 } 278 } 279 280 @Override onStartLoading()281 protected void onStartLoading() { 282 boolean isCursorStale = checkIfCursorStale(mResult); 283 if (mResult != null && !isCursorStale) { 284 deliverResult(mResult); 285 } 286 if (takeContentChanged() || mResult == null || isCursorStale) { 287 forceLoad(); 288 } 289 } 290 291 @Override onStopLoading()292 protected void onStopLoading() { 293 cancelLoad(); 294 } 295 296 @Override onCanceled(DirectoryResult result)297 public void onCanceled(DirectoryResult result) { 298 FileUtils.closeQuietly(result); 299 } 300 301 @Override onReset()302 protected void onReset() { 303 super.onReset(); 304 305 // Ensure the loader is stopped 306 onStopLoading(); 307 308 if (mResult != null && mResult.getCursor() != null && mObserver != null) { 309 mResult.getCursor().unregisterContentObserver(mObserver); 310 } 311 312 FileUtils.closeQuietly(mResult); 313 mResult = null; 314 } 315 checkIfCursorStale(DirectoryResult result)316 private boolean checkIfCursorStale(DirectoryResult result) { 317 if (result == null || result.getCursor() == null || result.getCursor().isClosed()) { 318 return true; 319 } 320 Cursor cursor = result.getCursor(); 321 try { 322 cursor.moveToPosition(-1); 323 for (int pos = 0; pos < cursor.getCount(); ++pos) { 324 if (!cursor.moveToNext()) { 325 return true; 326 } 327 } 328 } catch (Exception e) { 329 return true; 330 } 331 return false; 332 } 333 } 334