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