summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ivan Chiang <chiangi@google.com> 2018-08-08 18:54:14 +0800
committer Ivan Chiang <chiangi@google.com> 2018-12-11 13:31:26 +0800
commit282baa4f69a76091974e547a316fc65b762e3569 (patch)
tree79c453136ba3d0c831371f45fe91c896c2347414
parent52b0b2828c79345fd01cb3d39ff634897bd6b29f (diff)
Do the refactor in RecentsLoader and support global search
1. Do refactor in RecentsLoader. 2. Create MultiRootDocumentsLoader extracted from RecentsLoader. 3. Support only query Root.FLAG_LOCAL_ONLY roots in RecentsLoader. 4. Add GlobalSearchLoader to support global search in Recents root. Bug: 111695441 Fix: 119109832 Test: atest DocumentsUITests Change-Id: I19df6d58a7d1df042eef925575dc8d56cf81fa09
-rw-r--r--src/com/android/documentsui/AbstractActionHandler.java31
-rw-r--r--src/com/android/documentsui/GlobalSearchLoader.java109
-rw-r--r--src/com/android/documentsui/MultiRootDocumentsLoader.java443
-rw-r--r--src/com/android/documentsui/RecentsLoader.java365
-rw-r--r--src/com/android/documentsui/roots/ProvidersCache.java14
-rw-r--r--tests/common/com/android/documentsui/testing/TestDocumentsProvider.java45
-rw-r--r--tests/common/com/android/documentsui/testing/TestProvidersAccess.java3
-rw-r--r--tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java226
-rw-r--r--tests/unit/com/android/documentsui/RecentsLoaderTests.java28
-rw-r--r--tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java2
10 files changed, 907 insertions, 359 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 828e14ea1..a956b1717 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -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.");
- return new RecentsLoader(
- context,
- mProviders,
- mState,
- mInjector.features,
- mExecutors,
- mInjector.fileTypeLookup);
+ 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,
+ mExecutors,
+ mInjector.fileTypeLookup);
+ }
} else {
Uri contentsUri = mSearchMgr.isSearching()
diff --git a/src/com/android/documentsui/GlobalSearchLoader.java b/src/com/android/documentsui/GlobalSearchLoader.java
new file mode 100644
index 000000000..364d5ebc4
--- /dev/null
+++ b/src/com/android/documentsui/GlobalSearchLoader.java
@@ -0,0 +1,109 @@
+/*
+ * 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 */);
+ }
+ }
+}
diff --git a/src/com/android/documentsui/MultiRootDocumentsLoader.java b/src/com/android/documentsui/MultiRootDocumentsLoader.java
new file mode 100644
index 000000000..e2b7e297d
--- /dev/null
+++ b/src/com/android/documentsui/MultiRootDocumentsLoader.java
@@ -0,0 +1,443 @@
+/*
+ * 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;
+ }
+ }
+}
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index 0db270d96..4d29f7861 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -16,395 +16,78 @@
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.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.text.format.DateUtils;
-import android.util.Log;
-import com.android.documentsui.base.Features;
-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 androidx.annotation.GuardedBy;
-import androidx.loader.content.AsyncTaskLoader;
-
-import com.google.common.util.concurrent.AbstractFuture;
-
-import android.os.FileUtils;
-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;
-
-public class RecentsLoader 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_RECENTS = 4;
- private static final int MAX_OUTSTANDING_RECENTS_SVELTE = 2;
- /**
- * Time to wait for first pass to complete before returning partial results.
- */
- private static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;
-
- /** Maximum documents from a single root. */
- private static final int MAX_DOCS_FROM_ROOT = 64;
+public class RecentsLoader extends MultiRootDocumentsLoader {
/** Ignore documents older than this age. */
private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS;
/** MIME types that should always be excluded from recents. */
- private static final String[] RECENT_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
-
- private final Semaphore mQueryPermits;
-
- private final ProvidersAccess mProviders;
- private final State mState;
- private final Features mFeatures;
- private final Lookup<String, Executor> mExecutors;
- private final Lookup<String, String> mFileTypeMap;
+ private static final String[] REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR};
- @GuardedBy("mTasks")
- /** A authority -> RecentsTask map */
- private final Map<String, RecentsTask> mTasks = new HashMap<>();
-
- private CountDownLatch mFirstPassLatch;
- private volatile boolean mFirstPassDone;
-
- private DirectoryResult mResult;
+ /** Maximum documents from a single root. */
+ private static final int MAX_DOCS_FROM_ROOT = 64;
- public RecentsLoader(Context context, ProvidersAccess providers, State state, Features features,
+ public RecentsLoader(Context context, ProvidersAccess providers, State state,
Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap) {
-
- super(context);
- mProviders = providers;
- mState = state;
- mFeatures = features;
- 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_RECENTS_SVELTE : MAX_OUTSTANDING_RECENTS);
+ super(context, providers, state, executors, fileTypeMap);
}
@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<String>> rootsIndex = indexRecentsRoots();
-
- for (String authority : rootsIndex.keySet()) {
- mTasks.put(authority, new RecentsTask(authority, rootsIndex.get(authority)));
- }
-
- mFirstPassLatch = new CountDownLatch(mTasks.size());
- for (RecentsTask 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 = System.currentTimeMillis() - REJECT_OLDER_THAN;
-
- // Collect all finished tasks
- boolean allDone = true;
- int totalQuerySize = 0;
- List<Cursor> cursors = new ArrayList<>(mTasks.size());
- for (RecentsTask 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, RECENT_REJECT_MIMES, 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 Recents for authority: " + task.authority
- + ". Skip this authority in Recents.", e);
- }
- } else {
- allDone = false;
- }
- }
-
- if (DEBUG) {
- Log.d(TAG,
- "Found " + cursors.size() + " of " + totalQuerySize + " recent queries done");
- }
-
- final DirectoryResult result = new DirectoryResult();
-
- 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 notMovableMasked = new NotMovableMaskCursor(merged);
- final Cursor 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 -> rootIds
- */
- private Map<String, List<String>> indexRecentsRoots() {
- final Collection<RootInfo> roots = mProviders.getMatchingRootsBlocking(mState);
- HashMap<String, List<String>> rootsIndex = new HashMap<>();
- for (RootInfo root : roots) {
- if (!root.supportsRecents()) {
- continue;
- }
-
- if (!rootsIndex.containsKey(root.authority)) {
- rootsIndex.put(root.authority, new ArrayList<>());
- }
- rootsIndex.get(root.authority).add(root.rootId);
- }
-
- return rootsIndex;
- }
-
- @Override
- public void cancelLoadInBackground() {
- super.cancelLoadInBackground();
+ protected long getRejectBeforeTime() {
+ return System.currentTimeMillis() - REJECT_OLDER_THAN;
}
@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);
- }
+ protected String[] getRejectMimes() {
+ return REJECT_MIMES;
}
@Override
- protected void onStartLoading() {
- if (mResult != null) {
- deliverResult(mResult);
- }
- if (takeContentChanged() || mResult == null) {
- forceLoad();
- }
+ protected boolean shouldIgnoreRoot(RootInfo root) {
+ // only query the root is local only and support recents
+ return !root.isLocalOnly() || !root.supportsRecents();
}
@Override
- protected void onStopLoading() {
- cancelLoad();
+ protected boolean isDocumentsMovable() {
+ return false;
}
@Override
- public void onCanceled(DirectoryResult result) {
- FileUtils.closeQuietly(result);
+ protected QueryTask getQueryTask(String authority, List<RootInfo> rootInfos) {
+ return new RecentsTask(authority, rootInfos);
}
- @Override
- protected void onReset() {
- super.onReset();
+ private class RecentsTask extends QueryTask {
- // Ensure the loader is stopped
- onStopLoading();
-
- synchronized (mTasks) {
- for (RecentsTask 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 class RecentsTask extends AbstractFuture<Cursor[]> implements Runnable, Closeable {
- public final String authority;
- public final List<String> rootIds;
-
- private Cursor[] mCursors;
- private boolean mIsClosed = false;
-
- public RecentsTask(String authority, List<String> rootIds) {
- this.authority = authority;
- this.rootIds = rootIds;
+ public RecentsTask(String authority, List<RootInfo> rootInfos) {
+ super(authority, rootInfos);
}
@Override
- public void run() {
- if (isCancelled()) return;
-
- try {
- mQueryPermits.acquire();
- } catch (InterruptedException e) {
- return;
- }
-
- try {
- runInternal();
- } finally {
- mQueryPermits.release();
- }
- }
-
- private synchronized void runInternal() {
- if (mIsClosed) {
- return;
- }
-
- ContentProviderClient client = null;
- try {
- client = DocumentsApplication.acquireUnstableProviderOrThrow(
- getContext().getContentResolver(), authority);
-
- final Cursor[] res = new Cursor[rootIds.size()];
- mCursors = new Cursor[rootIds.size()];
- for (int i = 0; i < rootIds.size(); i++) {
- final Uri uri =
- DocumentsContract.buildRecentDocumentsUri(authority, rootIds.get(i));
- try {
- if (mFeatures.isContentPagingEnabled()) {
- final Bundle queryArgs = new Bundle();
- mState.sortModel.addQuerySortArgs(queryArgs);
- res[i] = client.query(uri, null, queryArgs, null);
- } else {
- res[i] = client.query(
- uri, null, null, null, mState.sortModel.getDocumentSortQuery());
- }
- mCursors[i] = new RootCursorWrapper(authority, rootIds.get(i), res[i],
- MAX_DOCS_FROM_ROOT);
- } catch (Exception e) {
- Log.w(TAG, "Failed to load " + authority + ", " + rootIds.get(i), 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;
- }
- }
-
- 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);
+ protected Uri getQueryUri(RootInfo rootInfo) {
+ return DocumentsContract.buildRecentDocumentsUri(authority, rootInfo.rootId);
}
@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 RootCursorWrapper generateResultCursor(RootInfo rootInfo, Cursor oriCursor) {
+ return new RootCursorWrapper(authority, rootInfo.rootId, oriCursor, MAX_DOCS_FROM_ROOT);
}
}
}
diff --git a/src/com/android/documentsui/roots/ProvidersCache.java b/src/com/android/documentsui/roots/ProvidersCache.java
index 6fcc8d887..266829047 100644
--- a/src/com/android/documentsui/roots/ProvidersCache.java
+++ b/src/com/android/documentsui/roots/ProvidersCache.java
@@ -107,13 +107,13 @@ public class ProvidersCache implements ProvidersAccess {
// Create a new anonymous "Recents" RootInfo. It's a faker.
mRecentsRoot = new RootInfo() {{
- // Special root for recents
- derivedIcon = R.drawable.ic_root_recent;
- derivedType = RootInfo.TYPE_RECENTS;
- flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD;
- title = mContext.getString(R.string.root_recent);
- availableBytes = -1;
- }};
+ // Special root for recents
+ derivedIcon = R.drawable.ic_root_recent;
+ derivedType = RootInfo.TYPE_RECENTS;
+ flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH;
+ title = mContext.getString(R.string.root_recent);
+ availableBytes = -1;
+ }};
}
private class RootsChangedObserver extends ContentObserver {
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java
index 9f30326a9..794d1c9fe 100644
--- a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java
+++ b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java
@@ -16,6 +16,7 @@
package com.android.documentsui.testing;
+import android.annotation.NonNull;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
@@ -90,6 +91,15 @@ public class TestDocumentsProvider extends DocumentsProvider {
}
@Override
+ public Cursor querySearchDocuments(String rootId, String query, String[] projection) {
+ if (mNextChildDocuments != null) {
+ return filterCursorByString(mNextChildDocuments, query);
+ }
+
+ return mNextChildDocuments;
+ }
+
+ @Override
public boolean onCreate() {
return true;
}
@@ -122,4 +132,39 @@ public class TestDocumentsProvider extends DocumentsProvider {
return cursor;
}
+
+ private static Cursor filterCursorByString(@NonNull Cursor cursor, String query) {
+ final int count = cursor.getCount();
+ final String[] columnNames = cursor.getColumnNames();
+
+ final MatrixCursor resultCursor = new MatrixCursor(columnNames, count);
+ cursor.moveToPosition(-1);
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ final int index = cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME);
+ if (!cursor.getString(index).contains(query)) {
+ continue;
+ }
+
+ final MatrixCursor.RowBuilder builder = resultCursor.newRow();
+ final int columnCount = cursor.getColumnCount();
+ for (int j = 0; j < columnCount; j++) {
+ final int type = cursor.getType(j);
+ switch (type) {
+ case Cursor.FIELD_TYPE_INTEGER:
+ builder.add(cursor.getLong(j));
+ break;
+
+ case Cursor.FIELD_TYPE_STRING:
+ builder.add(cursor.getString(j));
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+ cursor.moveToPosition(-1);
+ return resultCursor;
+ }
}
diff --git a/tests/common/com/android/documentsui/testing/TestProvidersAccess.java b/tests/common/com/android/documentsui/testing/TestProvidersAccess.java
index 71d7d8233..13c5c3427 100644
--- a/tests/common/com/android/documentsui/testing/TestProvidersAccess.java
+++ b/tests/common/com/android/documentsui/testing/TestProvidersAccess.java
@@ -62,6 +62,7 @@ public class TestProvidersAccess implements ProvidersAccess {
HOME.authority = Providers.AUTHORITY_STORAGE;
HOME.rootId = Providers.ROOT_ID_HOME;
HOME.title = "Home";
+ HOME.derivedType = RootInfo.TYPE_LOCAL;
HOME.flags = Root.FLAG_LOCAL_ONLY
| Root.FLAG_SUPPORTS_CREATE
| Root.FLAG_SUPPORTS_IS_CHILD
@@ -71,6 +72,8 @@ public class TestProvidersAccess implements ProvidersAccess {
HAMMY.authority = "yummies";
HAMMY.rootId = "hamsandwich";
HAMMY.title = "Ham Sandwich";
+ HAMMY.derivedType = RootInfo.TYPE_LOCAL;
+ HAMMY.flags = Root.FLAG_LOCAL_ONLY;
PICKLES = new RootInfo();
PICKLES.authority = "yummies";
diff --git a/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java b/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java
new file mode 100644
index 000000000..a0663f5f9
--- /dev/null
+++ b/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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 junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.database.Cursor;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.TextUtils;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.State;
+import com.android.documentsui.roots.RootCursorWrapper;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
+import com.android.documentsui.testing.ActivityManagers;
+import com.android.documentsui.testing.TestEnv;
+import com.android.documentsui.testing.TestFileTypeLookup;
+import com.android.documentsui.testing.TestImmediateExecutor;
+import com.android.documentsui.testing.TestProvidersAccess;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class GlobalSearchLoaderTest {
+
+ private static final String SEARCH_STRING = "freddy";
+ private static int FILE_FLAG = Document.FLAG_SUPPORTS_MOVE | Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_REMOVE;
+
+ private TestEnv mEnv;
+ private TestActivity mActivity;
+ private GlobalSearchLoader mLoader;
+
+ @Before
+ public void setUp() {
+ mEnv = TestEnv.create();
+ mActivity = TestActivity.create(mEnv);
+ mActivity.activityManager = ActivityManagers.create(false);
+
+ mEnv.state.action = State.ACTION_BROWSE;
+ mEnv.state.acceptMimes = new String[]{"*/*"};
+
+ mLoader = new GlobalSearchLoader(mActivity, mEnv.providers, mEnv.state,
+ TestImmediateExecutor.createLookup(), new TestFileTypeLookup(), SEARCH_STRING);
+
+ final DocumentInfo doc = mEnv.model.createFile(SEARCH_STRING + ".jpg", FILE_FLAG);
+ doc.lastModified = System.currentTimeMillis();
+ mEnv.mockProviders.get(TestProvidersAccess.HOME.authority)
+ .setNextChildDocumentsReturns(doc);
+
+ TestProvidersAccess.HOME.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ mEnv.state.showAdvanced = true;
+ }
+
+ @After
+ public void tearDown() {
+ TestProvidersAccess.HOME.flags &= ~DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ mEnv.state.showAdvanced = false;
+ }
+
+ @Test
+ public void testNotSearchableRoot_beIgnored() {
+ TestProvidersAccess.PICKLES.flags |= DocumentsContract.Root.FLAG_LOCAL_ONLY;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.PICKLES));
+ TestProvidersAccess.PICKLES.flags &= ~DocumentsContract.Root.FLAG_LOCAL_ONLY;
+ }
+
+ @Test
+ public void testNotLocalOnlyRoot_beIgnored() {
+ TestProvidersAccess.PICKLES.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.PICKLES));
+ TestProvidersAccess.PICKLES.flags &= ~DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ }
+
+ @Test
+ public void testShowAdvance_recentRoot_beIgnored() {
+ TestProvidersAccess.IMAGE.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.IMAGE));
+ TestProvidersAccess.IMAGE.flags &= ~DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ }
+
+ @Test
+ public void testShowAdvance_imageRoot_beIgnored() {
+ TestProvidersAccess.IMAGE.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
+ | DocumentsContract.Root.FLAG_LOCAL_ONLY;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.IMAGE));
+ TestProvidersAccess.IMAGE.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
+ | DocumentsContract.Root.FLAG_LOCAL_ONLY);
+ }
+
+ @Test
+ public void testShowAdvance_videoRoot_beIgnored() {
+ TestProvidersAccess.VIDEO.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
+ | DocumentsContract.Root.FLAG_LOCAL_ONLY;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.VIDEO));
+ TestProvidersAccess.VIDEO.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
+ | DocumentsContract.Root.FLAG_LOCAL_ONLY);
+ }
+
+ @Test
+ public void testShowAdvance_audioRoot_beIgnored() {
+ TestProvidersAccess.AUDIO.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
+ | DocumentsContract.Root.FLAG_LOCAL_ONLY;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.AUDIO));
+ TestProvidersAccess.AUDIO.flags &= ~(DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
+ | DocumentsContract.Root.FLAG_LOCAL_ONLY);
+ }
+
+ @Test
+ public void testShowAdvance_downloadRoot_beIgnored() {
+ TestProvidersAccess.DOWNLOADS.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.DOWNLOADS));
+ TestProvidersAccess.DOWNLOADS.flags &= ~DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ }
+
+ @Test
+ public void testSearchResult_includeDirectory() {
+ final DocumentInfo doc = mEnv.model.createFolder(SEARCH_STRING);
+ doc.lastModified = System.currentTimeMillis();
+
+ mEnv.mockProviders.get(TestProvidersAccess.HOME.authority)
+ .setNextChildDocumentsReturns(doc);
+
+ final DirectoryResult result = mLoader.loadInBackground();
+
+ final Cursor c = result.cursor;
+ assertEquals(1, c.getCount());
+
+ c.moveToNext();
+ final String mimeType = c.getString(c.getColumnIndex(Document.COLUMN_MIME_TYPE));
+ assertEquals(Document.MIME_TYPE_DIR, mimeType);
+ }
+
+ @Test
+ public void testSearchResult_isMovable() {
+ final DirectoryResult result = mLoader.loadInBackground();
+
+ final Cursor c = result.cursor;
+ assertEquals(1, c.getCount());
+
+ c.moveToNext();
+ final int flags = c.getInt(c.getColumnIndex(Document.COLUMN_FLAGS));
+ assertEquals(FILE_FLAG, flags);
+ }
+
+ @Test
+ public void testSearchResult_includeSearchString() {
+ final DocumentInfo pdfDoc = mEnv.model.createFile(SEARCH_STRING + ".pdf");
+ pdfDoc.lastModified = System.currentTimeMillis();
+
+ final DocumentInfo apkDoc = mEnv.model.createFile(SEARCH_STRING + ".apk");
+ apkDoc.lastModified = System.currentTimeMillis();
+
+ final DocumentInfo testApkDoc = mEnv.model.createFile("test.apk");
+ testApkDoc.lastModified = System.currentTimeMillis();
+
+ mEnv.mockProviders.get(TestProvidersAccess.HOME.authority)
+ .setNextChildDocumentsReturns(pdfDoc, apkDoc, testApkDoc);
+
+ mEnv.state.sortModel.sortByUser(
+ SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
+
+ final DirectoryResult result = mLoader.loadInBackground();
+ final Cursor c = result.cursor;
+
+ assertEquals(2, c.getCount());
+
+ c.moveToNext();
+ String displayName = c.getString(c.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
+ assertTrue(displayName.contains(SEARCH_STRING));
+
+ c.moveToNext();
+ displayName = c.getString(c.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
+ assertTrue(displayName.contains(SEARCH_STRING));
+ }
+
+ @Test
+ public void testSearchResult_includeDifferentRoot() {
+ final DocumentInfo pdfDoc = mEnv.model.createFile(SEARCH_STRING + ".pdf");
+ pdfDoc.lastModified = System.currentTimeMillis();
+
+ final DocumentInfo apkDoc = mEnv.model.createFile(SEARCH_STRING + ".apk");
+ apkDoc.lastModified = System.currentTimeMillis();
+
+ final DocumentInfo testApkDoc = mEnv.model.createFile("test.apk");
+ testApkDoc.lastModified = System.currentTimeMillis();
+
+ mEnv.mockProviders.get(TestProvidersAccess.HAMMY.authority)
+ .setNextChildDocumentsReturns(pdfDoc, apkDoc, testApkDoc);
+
+ TestProvidersAccess.HAMMY.flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+
+ mEnv.state.sortModel.sortByUser(
+ SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
+
+ final DirectoryResult result = mLoader.loadInBackground();
+ final Cursor c = result.cursor;
+
+ assertEquals(3, c.getCount());
+
+ TestProvidersAccess.HAMMY.flags &= ~DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
+ }
+}
diff --git a/tests/unit/com/android/documentsui/RecentsLoaderTests.java b/tests/unit/com/android/documentsui/RecentsLoaderTests.java
index 657432fef..eb7528019 100644
--- a/tests/unit/com/android/documentsui/RecentsLoaderTests.java
+++ b/tests/unit/com/android/documentsui/RecentsLoaderTests.java
@@ -17,6 +17,7 @@
package com.android.documentsui;
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import android.database.Cursor;
@@ -28,7 +29,6 @@ import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.testing.ActivityManagers;
import com.android.documentsui.testing.TestEnv;
-import com.android.documentsui.testing.TestFeatures;
import com.android.documentsui.testing.TestFileTypeLookup;
import com.android.documentsui.testing.TestImmediateExecutor;
import com.android.documentsui.testing.TestProvidersAccess;
@@ -54,11 +54,35 @@ public class RecentsLoaderTests {
mEnv.state.action = State.ACTION_BROWSE;
mEnv.state.acceptMimes = new String[] { "*/*" };
- mLoader = new RecentsLoader(mActivity, mEnv.providers, mEnv.state, mEnv.features,
+ mLoader = new RecentsLoader(mActivity, mEnv.providers, mEnv.state,
TestImmediateExecutor.createLookup(), new TestFileTypeLookup());
}
@Test
+ public void testNotLocalOnlyRoot_beIgnored() {
+ assertTrue(mLoader.shouldIgnoreRoot(TestProvidersAccess.PICKLES));
+ }
+
+ @Test
+ public void testLocalOnlyRoot_supportRecent_notIgnored() {
+ assertFalse(mLoader.shouldIgnoreRoot(TestProvidersAccess.DOWNLOADS));
+ }
+
+ @Test
+ public void testDocumentsNotIncludeDirectory() {
+ final DocumentInfo doc = mEnv.model.createFolder("test");
+ doc.lastModified = System.currentTimeMillis();
+
+ mEnv.mockProviders.get(TestProvidersAccess.HOME.authority)
+ .setNextChildDocumentsReturns(doc);
+
+ final DirectoryResult result = mLoader.loadInBackground();
+
+ final Cursor c = result.cursor;
+ assertEquals(0, c.getCount());
+ }
+
+ @Test
public void testDocumentsNotMovable() {
final DocumentInfo doc = mEnv.model.createFile("freddy.jpg",
Document.FLAG_SUPPORTS_MOVE
diff --git a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java
index 6028eacdf..16414e55b 100644
--- a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java
+++ b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java
@@ -53,8 +53,8 @@ public class RootsFragmentTest {
TestProvidersAccess.DOWNLOADS.title,
"" /* SpacerItem */,
TestProvidersAccess.EXTERNALSTORAGE.title,
- "" /* SpacerItem */,
TestProvidersAccess.HAMMY.title,
+ "" /* SpacerItem */,
TestProvidersAccess.INSPECTOR.title,
TestProvidersAccess.PICKLES.title};