Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c19c2a75 authored by Ivan Chiang's avatar Ivan Chiang Committed by Android (Google) Code Review
Browse files

Merge "Do the refactor in RecentsLoader and support global search"

parents b7e1a798 282baa4f
Loading
Loading
Loading
Loading
+23 −8
Original line number Diff line number Diff line
@@ -567,14 +567,29 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA

            if (mState.stack.isRecents()) {

                if (DEBUG) Log.d(TAG, "Creating new loader recents.");
                if (mSearchMgr.isSearching()) {
                    if (DEBUG) {
                        Log.d(TAG, "Creating new GlobalSearchloader.");
                    }

                    return new GlobalSearchLoader(
                            context,
                            mProviders,
                            mState,
                            mExecutors,
                            mInjector.fileTypeLookup,
                            mSearchMgr.getCurrentSearch());
                } else {
                    if (DEBUG) {
                        Log.d(TAG, "Creating new loader recents.");
                    }
                    return new RecentsLoader(
                            context,
                            mProviders,
                            mState,
                        mInjector.features,
                            mExecutors,
                            mInjector.fileTypeLookup);
                }
            } else {

                Uri contentsUri = mSearchMgr.isSearching()
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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 android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;

import androidx.annotation.NonNull;

import com.android.documentsui.base.Lookup;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.roots.RootCursorWrapper;

import java.util.List;
import java.util.concurrent.Executor;

/*
 * The class to query multiple roots support {@link DocumentsContract.Root#FLAG_LOCAL_ONLY}
 * and {@link DocumentsContract.Root#FLAG_SUPPORTS_SEARCH} from
 * {@link android.provider.DocumentsProvider}.
 */
public class GlobalSearchLoader extends MultiRootDocumentsLoader {
    private final String mSearchString;

    /*
     * Create the loader to query multiple roots support
     * {@link DocumentsContract.Root#FLAG_LOCAL_ONLY} and
     * {@link DocumentsContract.Root#FLAG_SUPPORTS_SEARCH} from
     * {@link android.provider.DocumentsProvider}.
     *
     * @param context the context
     * @param providers the providers
     * @param state current state
     * @param features the feature flags
     * @param executors the executors of authorities
     * @param fileTypeMap the map of mime types and file types.
     * @param searchString the string for searching
     */
    public GlobalSearchLoader(Context context, ProvidersAccess providers, State state,
            Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap,
            String searchString) {
        super(context, providers, state, executors, fileTypeMap);
        mSearchString = searchString;
    }

    @Override
    protected boolean shouldIgnoreRoot(RootInfo root) {
        // Only support local search in GlobalSearchLoader
        if (!root.isLocalOnly() || !root.supportsSearch()) {
            return true;
        }

        // If the value of showAdvanced is true,
        // don't query media roots and downloads root to avoid showing
        // duplicated files.
        if (mState.showAdvanced && (root.isLibrary() || root.isDownloads())) {
            return true;
        }
        return false;
    }

    @Override
    protected QueryTask getQueryTask(String authority, List<RootInfo> rootInfos) {
        return new SearchTask(authority, rootInfos);
    }

    private class SearchTask extends QueryTask {

        public SearchTask(String authority, List<RootInfo> rootInfos) {
            super(authority, rootInfos);
        }

        @Override
        protected void addQueryArgs(@NonNull Bundle queryArgs) {
            queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, mSearchString);
            queryArgs.putBoolean(DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, true);
        }

        @Override
        protected Uri getQueryUri(RootInfo rootInfo) {
            return DocumentsContract.buildSearchDocumentsUri(authority,
                    rootInfo.rootId, mSearchString);
        }

        @Override
        protected RootCursorWrapper generateResultCursor(RootInfo rootInfo, Cursor oriCursor) {
            return new RootCursorWrapper(authority, rootInfo.rootId, oriCursor, -1 /* maxCount */);
        }
    }
}
+443 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.DEBUG;
import static com.android.documentsui.base.SharedMinimal.TAG;

import android.app.ActivityManager;
import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.FileUtils;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.loader.content.AsyncTaskLoader;

import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.FilteringCursorWrapper;
import com.android.documentsui.base.Lookup;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.roots.RootCursorWrapper;

import com.google.common.util.concurrent.AbstractFuture;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/*
 * The abstract class to query multiple roots from {@link android.provider.DocumentsProvider}
 * and return the combined result.
 */
public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<DirectoryResult> {
    // TODO: clean up cursor ownership so background thread doesn't traverse
    // previously returned cursors for filtering/sorting; this currently races
    // with the UI thread.

    private static final int MAX_OUTSTANDING_TASK = 4;
    private static final int MAX_OUTSTANDING_TASK_SVELTE = 2;

    /**
     * Time to wait for first pass to complete before returning partial results.
     */
    private static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;

    protected final State mState;

    private final Semaphore mQueryPermits;
    private final ProvidersAccess mProviders;
    private final Lookup<String, Executor> mExecutors;
    private final Lookup<String, String> mFileTypeMap;

    @GuardedBy("mTasks")
    /** A authority -> QueryTask map */
    private final Map<String, QueryTask> mTasks = new HashMap<>();

    private CountDownLatch mFirstPassLatch;
    private volatile boolean mFirstPassDone;

    private DirectoryResult mResult;

    /*
     * Create the loader to query roots from {@link android.provider.DocumentsProvider}.
     *
     * @param context the context
     * @param providers the providers
     * @param state current state
     * @param executors the executors of authorities
     * @param fileTypeMap the map of mime types and file types.
     */
    public MultiRootDocumentsLoader(Context context, ProvidersAccess providers, State state,
            Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap) {

        super(context);
        mProviders = providers;
        mState = state;
        mExecutors = executors;
        mFileTypeMap = fileTypeMap;

        // Keep clients around on high-RAM devices, since we'd be spinning them
        // up moments later to fetch thumbnails anyway.
        final ActivityManager am = (ActivityManager) getContext().getSystemService(
                Context.ACTIVITY_SERVICE);
        mQueryPermits = new Semaphore(
                am.isLowRamDevice() ? MAX_OUTSTANDING_TASK_SVELTE : MAX_OUTSTANDING_TASK);
    }

    @Override
    public DirectoryResult loadInBackground() {
        synchronized (mTasks) {
            return loadInBackgroundLocked();
        }
    }

    private DirectoryResult loadInBackgroundLocked() {
        if (mFirstPassLatch == null) {
            // First time through we kick off all the recent tasks, and wait
            // around to see if everyone finishes quickly.
            Map<String, List<RootInfo>> rootsIndex = indexRoots();

            for (Map.Entry<String, List<RootInfo>> rootEntry : rootsIndex.entrySet()) {
                mTasks.put(rootEntry.getKey(),
                        getQueryTask(rootEntry.getKey(), rootEntry.getValue()));
            }

            mFirstPassLatch = new CountDownLatch(mTasks.size());
            for (QueryTask task : mTasks.values()) {
                mExecutors.lookup(task.authority).execute(task);
            }

            try {
                mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
                mFirstPassDone = true;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        final long rejectBefore = getRejectBeforeTime();

        // Collect all finished tasks
        boolean allDone = true;
        int totalQuerySize = 0;
        List<Cursor> cursors = new ArrayList<>(mTasks.size());
        for (QueryTask task : mTasks.values()) {
            if (task.isDone()) {
                try {
                    final Cursor[] taskCursors = task.get();
                    if (taskCursors == null || taskCursors.length == 0) {
                        continue;
                    }

                    totalQuerySize += taskCursors.length;
                    for (Cursor cursor : taskCursors) {
                        if (cursor == null) {
                            // It's possible given an authority, some roots fail to return a cursor
                            // after a query.
                            continue;
                        }
                        final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
                                cursor, mState.acceptMimes, getRejectMimes(), rejectBefore) {
                            @Override
                            public void close() {
                                // Ignored, since we manage cursor lifecycle internally
                            }
                        };
                        cursors.add(filtered);
                    }

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (ExecutionException e) {
                    // We already logged on other side
                } catch (Exception e) {
                    // Catch exceptions thrown when we read the cursor.
                    Log.e(TAG, "Failed to query documents for authority: " + task.authority
                            + ". Skip this authority.", e);
                }
            } else {
                allDone = false;
            }
        }

        if (DEBUG) {
            Log.d(TAG,
                    "Found " + cursors.size() + " of " + totalQuerySize + " queries done");
        }

        final DirectoryResult result = new DirectoryResult();
        result.doc = new DocumentInfo();

        final Cursor merged;
        if (cursors.size() > 0) {
            merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
        } else {
            // Return something when nobody is ready
            merged = new MatrixCursor(new String[0]);
        }

        final Cursor sorted;
        if (isDocumentsMovable()) {
            sorted = mState.sortModel.sortCursor(merged, mFileTypeMap);
        } else {
            final Cursor notMovableMasked = new NotMovableMaskCursor(merged);
            sorted = mState.sortModel.sortCursor(notMovableMasked, mFileTypeMap);
        }

        // Tell the UI if this is an in-progress result. When loading is complete, another update is
        // sent with EXTRA_LOADING set to false.
        Bundle extras = new Bundle();
        extras.putBoolean(DocumentsContract.EXTRA_LOADING, !allDone);
        sorted.setExtras(extras);

        result.cursor = sorted;

        return result;
    }

    /**
     * Returns a map of Authority -> rootInfos.
     */
    private Map<String, List<RootInfo>> indexRoots() {
        final Collection<RootInfo> roots = mProviders.getMatchingRootsBlocking(mState);
        HashMap<String, List<RootInfo>> rootsIndex = new HashMap<>();
        for (RootInfo root : roots) {
            // ignore the root with authority is null. e.g. Recent
            if (root.authority == null || shouldIgnoreRoot(root)) {
                continue;
            }

            if (!rootsIndex.containsKey(root.authority)) {
                rootsIndex.put(root.authority, new ArrayList<>());
            }
            rootsIndex.get(root.authority).add(root);
        }

        return rootsIndex;
    }

    protected long getRejectBeforeTime() {
        return -1;
    }

    protected String[] getRejectMimes() {
        return null;
    }

    protected boolean shouldIgnoreRoot(RootInfo root) {
        return false;
    }

    protected boolean isDocumentsMovable() {
        return true;
    }

    protected abstract QueryTask getQueryTask(String authority, List<RootInfo> rootInfos);

    @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() {
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() || mResult == null) {
            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();

        synchronized (mTasks) {
            for (QueryTask task : mTasks.values()) {
                FileUtils.closeQuietly(task);
            }
        }

        FileUtils.closeQuietly(mResult);
        mResult = null;
    }

    // TODO: create better transfer of ownership around cursor to ensure its
    // closed in all edge cases.

    private static class NotMovableMaskCursor extends CursorWrapper {
        private static final int NOT_MOVABLE_MASK =
                ~(Document.FLAG_SUPPORTS_DELETE
                        | Document.FLAG_SUPPORTS_REMOVE
                        | Document.FLAG_SUPPORTS_MOVE);

        private NotMovableMaskCursor(Cursor cursor) {
            super(cursor);
        }

        @Override
        public int getInt(int index) {
            final int flagIndex = getWrappedCursor().getColumnIndex(Document.COLUMN_FLAGS);
            final int value = super.getInt(index);
            return (index == flagIndex) ? (value & NOT_MOVABLE_MASK) : value;
        }
    }

    protected abstract class QueryTask extends AbstractFuture<Cursor[]> implements Runnable,
            Closeable {
        public final String authority;
        public final List<RootInfo> rootInfos;

        private Cursor[] mCursors;
        private boolean mIsClosed = false;

        public QueryTask(String authority, List<RootInfo> rootInfos) {
            this.authority = authority;
            this.rootInfos = rootInfos;
        }

        @Override
        public void run() {
            if (isCancelled()) {
                return;
            }

            try {
                mQueryPermits.acquire();
            } catch (InterruptedException e) {
                return;
            }

            try {
                runInternal();
            } finally {
                mQueryPermits.release();
            }
        }

        protected abstract Uri getQueryUri(RootInfo rootInfo);

        protected abstract RootCursorWrapper generateResultCursor(RootInfo rootInfo,
                Cursor oriCursor);

        protected void addQueryArgs(@NonNull Bundle queryArgs) {
        }

        private synchronized void runInternal() {
            if (mIsClosed) {
                return;
            }

            ContentProviderClient client = null;
            try {
                client = DocumentsApplication.acquireUnstableProviderOrThrow(
                        getContext().getContentResolver(), authority);

                final int rootInfoCount = rootInfos.size();
                final Cursor[] res = new Cursor[rootInfoCount];
                mCursors = new Cursor[rootInfoCount];

                for (int i = 0; i < rootInfoCount; i++) {
                    final Uri uri = getQueryUri(rootInfos.get(i));
                    try {
                        final Bundle queryArgs = new Bundle();
                        mState.sortModel.addQuerySortArgs(queryArgs);
                        addQueryArgs(queryArgs);
                        res[i] = client.query(uri, null, queryArgs, null);
                        mCursors[i] = generateResultCursor(rootInfos.get(i), res[i]);
                    } catch (Exception e) {
                        Log.w(TAG, "Failed to load " + authority + ", " + rootInfos.get(i).rootId,
                                e);
                    }
                }

            } catch (Exception e) {
                Log.w(TAG, "Failed to acquire content resolver for authority: " + authority);
            } finally {
                ContentProviderClient.closeQuietly(client);
            }

            set(mCursors);

            mFirstPassLatch.countDown();
            if (mFirstPassDone) {
                onContentChanged();
            }
        }

        @Override
        public synchronized void close() throws IOException {
            if (mCursors == null) {
                return;
            }

            for (Cursor cursor : mCursors) {
                FileUtils.closeQuietly(cursor);
            }

            mIsClosed = true;
        }
    }
}
+24 −341

File changed.

Preview size limit exceeded, changes collapsed.

+7 −7
Original line number Diff line number Diff line
@@ -110,7 +110,7 @@ public class ProvidersCache implements ProvidersAccess {
            // Special root for recents
            derivedIcon = R.drawable.ic_root_recent;
            derivedType = RootInfo.TYPE_RECENTS;
                flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD;
            flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH;
            title = mContext.getString(R.string.root_recent);
            availableBytes = -1;
        }};
Loading