/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import static com.android.documentsui.base.SharedMinimal.VERBOSE; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.MergeCursor; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.FileUtils; import android.os.OperationCanceledException; import android.os.RemoteException; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.util.Log; import androidx.annotation.Nullable; import androidx.loader.content.AsyncTaskLoader; import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DebugFlags; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Features; import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.Lookup; import com.android.documentsui.base.MimeTypes; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.roots.RootCursorWrapper; import com.android.documentsui.sorting.SortModel; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; public class DirectoryLoader extends AsyncTaskLoader { private static final String TAG = "DirectoryLoader"; private static final String[] SEARCH_REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR}; private static final String[] PHOTO_PICKING_ACCEPT_MIMES = new String[] {Document.MIME_TYPE_DIR, MimeTypes.IMAGE_MIME}; private final LockingContentObserver mObserver; private final RootInfo mRoot; private final State mState; private final Uri mUri; private final SortModel mModel; private final Lookup mFileTypeLookup; private final boolean mSearchMode; private final Bundle mQueryArgs; private final boolean mPhotoPicking; @Nullable private DocumentInfo mDoc; private CancellationSignal mSignal; private DirectoryResult mResult; private Features mFeatures; public DirectoryLoader( Features features, Context context, State state, Uri uri, Lookup fileTypeLookup, ContentLock lock, Bundle queryArgs) { super(context); mFeatures = features; mState = state; mRoot = state.stack.getRoot(); mUri = uri; mModel = state.sortModel; mDoc = state.stack.peek(); mFileTypeLookup = fileTypeLookup; mSearchMode = queryArgs != null; mQueryArgs = queryArgs; mObserver = new LockingContentObserver(lock, this::onContentChanged); mPhotoPicking = state.isPhotoPicking(); } @Override protected Executor getExecutor() { return ProviderExecutor.forAuthority(mRoot.authority); } @Override public final DirectoryResult loadInBackground() { synchronized (this) { if (isLoadInBackgroundCanceled()) { throw new OperationCanceledException(); } mSignal = new CancellationSignal(); } final String authority = mUri.getAuthority(); final DirectoryResult result = new DirectoryResult(); result.doc = mDoc; ContentProviderClient client = null; Cursor cursor; try { final Bundle queryArgs = new Bundle(); mModel.addQuerySortArgs(queryArgs); final List userIds = new ArrayList<>(); if (mSearchMode) { queryArgs.putAll(mQueryArgs); if (shouldSearchAcrossProfile()) { for (UserId userId : DocumentsApplication.getUserIdManager( getContext()).getUserIds()) { if (mState.canInteractWith(userId)) { userIds.add(userId); } } } } if (userIds.isEmpty()) { userIds.add(mRoot.userId); } if (userIds.size() == 1) { if (!mState.canInteractWith(mRoot.userId)) { result.exception = new CrossProfileNoPermissionException(); return result; } else if (mRoot.userId.isQuietModeEnabled(getContext())) { result.exception = new CrossProfileQuietModeException(mRoot.userId); return result; } else if (mDoc == null) { // TODO (b/35996595): Consider plumbing through the actual exception, though it // might not be very useful (always pointing to // DatabaseUtils#readExceptionFromParcel()). result.exception = new IllegalStateException("Failed to load root document."); return result; } } if (mDoc != null && mDoc.isInArchive()) { final ContentResolver resolver = mRoot.userId.getContentResolver(getContext()); client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); ArchivesProvider.acquireArchive(client, mUri); result.client = client; } if (mFeatures.isContentPagingEnabled()) { // TODO: At some point we don't want forced flags to override real paging... // and that point is when we have real paging. DebugFlags.addForcedPagingArgs(queryArgs); } cursor = queryOnUsers(userIds, authority, queryArgs); if (cursor == null) { throw new RemoteException("Provider returned null"); } cursor.registerContentObserver(mObserver); FilteringCursorWrapper filteringCursor = new FilteringCursorWrapper(cursor); filteringCursor.filterHiddenFiles(mState.showHiddenFiles); if (mSearchMode && !mFeatures.isFoldersInSearchResultsEnabled()) { // There is no findDocumentPath API. Enable filtering on folders in search mode. filteringCursor.filterMimes(/* acceptMimes= */ null, SEARCH_REJECT_MIMES); } if (mPhotoPicking) { filteringCursor.filterMimes(PHOTO_PICKING_ACCEPT_MIMES, /* rejectMimes= */ null); } cursor = filteringCursor; // TODO: When API tweaks have landed, use ContentResolver.EXTRA_HONORED_ARGS // instead of checking directly for ContentResolver.QUERY_ARG_SORT_COLUMNS (won't work) if (mFeatures.isContentPagingEnabled() && cursor.getExtras().containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { if (VERBOSE) Log.d(TAG, "Skipping sort of pre-sorted cursor. Booya!"); } else { cursor = mModel.sortCursor(cursor, mFileTypeLookup); } result.setCursor(cursor); } catch (Exception e) { Log.w(TAG, "Failed to query", e); result.exception = e; FileUtils.closeQuietly(client); } finally { synchronized (this) { mSignal = null; } } return result; } private boolean shouldSearchAcrossProfile() { return mState.supportsCrossProfile() && mRoot.supportsCrossProfile() && mQueryArgs.containsKey(DocumentsContract.QUERY_ARG_DISPLAY_NAME); } @Nullable private Cursor queryOnUsers(List userIds, String authority, Bundle queryArgs) throws RemoteException { final List cursors = new ArrayList<>(userIds.size()); for (UserId userId : userIds) { try (ContentProviderClient userClient = DocumentsApplication.acquireUnstableProviderOrThrow( userId.getContentResolver(getContext()), authority)) { Cursor c = userClient.query(mUri, /* projection= */null, queryArgs, mSignal); if (c != null) { cursors.add(new RootCursorWrapper(userId, mUri.getAuthority(), mRoot.rootId, c, /* maxCount= */-1)); } } catch (RemoteException e) { Log.d(TAG, "Failed to query for user " + userId, e); // Searching on other profile may not succeed because profile may be in quiet mode. if (UserId.CURRENT_USER.equals(userId)) { throw e; } } } int size = cursors.size(); switch (size) { case 0: return null; case 1: return cursors.get(0); default: return new MergeCursor(cursors.toArray(new Cursor[size])); } } @Override public void cancelLoadInBackground() { super.cancelLoadInBackground(); synchronized (this) { if (mSignal != null) { mSignal.cancel(); } } } @Override public void deliverResult(DirectoryResult result) { if (isReset()) { FileUtils.closeQuietly(result); return; } DirectoryResult oldResult = mResult; mResult = result; if (isStarted()) { super.deliverResult(result); } if (oldResult != null && oldResult != result) { FileUtils.closeQuietly(oldResult); } } @Override protected void onStartLoading() { boolean isCursorStale = checkIfCursorStale(mResult); if (mResult != null && !isCursorStale) { deliverResult(mResult); } if (takeContentChanged() || mResult == null || isCursorStale) { forceLoad(); } } @Override protected void onStopLoading() { cancelLoad(); } @Override public void onCanceled(DirectoryResult result) { FileUtils.closeQuietly(result); } @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); if (mResult != null && mResult.getCursor() != null && mObserver != null) { mResult.getCursor().unregisterContentObserver(mObserver); } FileUtils.closeQuietly(mResult); mResult = null; } private boolean checkIfCursorStale(DirectoryResult result) { if (result == null || result.getCursor() == null || result.getCursor().isClosed()) { return true; } Cursor cursor = result.getCursor(); try { cursor.moveToPosition(-1); for (int pos = 0; pos < cursor.getCount(); ++pos) { if (!cursor.moveToNext()) { return true; } } } catch (Exception e) { return true; } return false; } }