New recents behavior to match spec.
Updated behavior queries each documents root for recently modified documents. It uses a new variant of DirectoryLoader which limits the maximum number of parallel queries to relieve memory pressure. When first started, it waits up to 500ms for everyone to finish, then publishes whatever results are ready, and then refreshes results as each straggler finishes. New RootCursorWrapper that always blends in authority and rootId columns, which are used for binding root details from blended cursors. Bug: 10593596, 10329994 Change-Id: Icc0d4a2f1b6166edc72f78a4c88f444eeba6f2f0
This commit is contained in:
@@ -20831,6 +20831,7 @@ package android.provider {
|
||||
field public static final int FLAG_PROVIDES_IMAGES = 32; // 0x20
|
||||
field public static final int FLAG_PROVIDES_VIDEO = 16; // 0x10
|
||||
field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
|
||||
field public static final int FLAG_SUPPORTS_RECENTS = 64; // 0x40
|
||||
field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
|
||||
field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
|
||||
field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
|
||||
|
||||
@@ -411,6 +411,15 @@ public final class DocumentsContract {
|
||||
* @see Intent#EXTRA_MIME_TYPES
|
||||
*/
|
||||
public static final int FLAG_PROVIDES_IMAGES = 1 << 5;
|
||||
|
||||
/**
|
||||
* Flag indicating that this root can report recently modified
|
||||
* documents.
|
||||
*
|
||||
* @see #COLUMN_FLAGS
|
||||
* @see DocumentsContract#buildRecentDocumentsUri(String, String)
|
||||
*/
|
||||
public static final int FLAG_SUPPORTS_RECENTS = 1 << 6;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ LOCAL_MODULE_TAGS := optional
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-subdir-java-files)
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 guava
|
||||
|
||||
LOCAL_PACKAGE_NAME := DocumentsUI
|
||||
LOCAL_CERTIFICATE := platform
|
||||
|
||||
@@ -64,6 +64,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.android.documentsui.DocumentsActivity.State;
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.RootInfo;
|
||||
import com.android.internal.util.Predicate;
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
@@ -86,6 +87,7 @@ public class DirectoryFragment extends Fragment {
|
||||
|
||||
public static final int TYPE_NORMAL = 1;
|
||||
public static final int TYPE_SEARCH = 2;
|
||||
public static final int TYPE_RECENT_OPEN = 3;
|
||||
|
||||
private int mType = TYPE_NORMAL;
|
||||
|
||||
@@ -95,7 +97,10 @@ public class DirectoryFragment extends Fragment {
|
||||
private LoaderCallbacks<DirectoryResult> mCallbacks;
|
||||
|
||||
private static final String EXTRA_TYPE = "type";
|
||||
private static final String EXTRA_URI = "uri";
|
||||
private static final String EXTRA_AUTHORITY = "authority";
|
||||
private static final String EXTRA_ROOT_ID = "rootId";
|
||||
private static final String EXTRA_DOC_ID = "docId";
|
||||
private static final String EXTRA_QUERY = "query";
|
||||
|
||||
private static AtomicInteger sLoaderId = new AtomicInteger(4000);
|
||||
|
||||
@@ -104,24 +109,26 @@ public class DirectoryFragment extends Fragment {
|
||||
private final int mLoaderId = sLoaderId.incrementAndGet();
|
||||
|
||||
public static void showNormal(FragmentManager fm, Uri uri) {
|
||||
show(fm, TYPE_NORMAL, uri);
|
||||
show(fm, TYPE_NORMAL, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), null);
|
||||
}
|
||||
|
||||
public static void showSearch(FragmentManager fm, Uri uri, String query) {
|
||||
final Uri searchUri = DocumentsContract.buildSearchDocumentsUri(
|
||||
uri.getAuthority(), DocumentsContract.getDocumentId(uri), query);
|
||||
show(fm, TYPE_SEARCH, searchUri);
|
||||
show(fm, TYPE_SEARCH, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri),
|
||||
query);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static void showRecentsOpen(FragmentManager fm) {
|
||||
// TODO: new recents behavior
|
||||
show(fm, TYPE_RECENT_OPEN, null, null, null, null);
|
||||
}
|
||||
|
||||
private static void show(FragmentManager fm, int type, Uri uri) {
|
||||
private static void show(FragmentManager fm, int type, String authority, String rootId,
|
||||
String docId, String query) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putInt(EXTRA_TYPE, type);
|
||||
args.putParcelable(EXTRA_URI, uri);
|
||||
args.putString(EXTRA_AUTHORITY, authority);
|
||||
args.putString(EXTRA_ROOT_ID, rootId);
|
||||
args.putString(EXTRA_DOC_ID, docId);
|
||||
args.putString(EXTRA_QUERY, query);
|
||||
|
||||
final DirectoryFragment fragment = new DirectoryFragment();
|
||||
fragment.setArguments(args);
|
||||
@@ -160,9 +167,8 @@ public class DirectoryFragment extends Fragment {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final Context context = getActivity();
|
||||
final Uri uri = getArguments().getParcelable(EXTRA_URI);
|
||||
|
||||
mAdapter = new DocumentsAdapter(uri.getAuthority());
|
||||
mAdapter = new DocumentsAdapter();
|
||||
mType = getArguments().getInt(EXTRA_TYPE);
|
||||
|
||||
mCallbacks = new LoaderCallbacks<DirectoryResult>() {
|
||||
@@ -170,15 +176,26 @@ public class DirectoryFragment extends Fragment {
|
||||
public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
|
||||
final State state = getDisplayState(DirectoryFragment.this);
|
||||
|
||||
Uri contentsUri;
|
||||
if (mType == TYPE_NORMAL) {
|
||||
contentsUri = DocumentsContract.buildChildDocumentsUri(
|
||||
uri.getAuthority(), DocumentsContract.getDocumentId(uri));
|
||||
} else {
|
||||
contentsUri = uri;
|
||||
}
|
||||
final String authority = getArguments().getString(EXTRA_AUTHORITY);
|
||||
final String rootId = getArguments().getString(EXTRA_ROOT_ID);
|
||||
final String docId = getArguments().getString(EXTRA_DOC_ID);
|
||||
final String query = getArguments().getString(EXTRA_QUERY);
|
||||
|
||||
return new DirectoryLoader(context, contentsUri, state.sortOrder);
|
||||
Uri contentsUri;
|
||||
switch (mType) {
|
||||
case TYPE_NORMAL:
|
||||
contentsUri = DocumentsContract.buildChildDocumentsUri(authority, docId);
|
||||
return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
|
||||
case TYPE_SEARCH:
|
||||
contentsUri = DocumentsContract.buildSearchDocumentsUri(
|
||||
authority, docId, query);
|
||||
return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
|
||||
case TYPE_RECENT_OPEN:
|
||||
return new RecentLoader(context);
|
||||
default:
|
||||
throw new IllegalStateException("Unknown type " + mType);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -246,8 +263,7 @@ public class DirectoryFragment extends Fragment {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final Cursor cursor = mAdapter.getItem(position);
|
||||
final Uri uri = getArguments().getParcelable(EXTRA_URI);
|
||||
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
|
||||
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
|
||||
if (mFilter.apply(doc)) {
|
||||
((DocumentsActivity) getActivity()).onDocumentPicked(doc);
|
||||
}
|
||||
@@ -285,8 +301,7 @@ public class DirectoryFragment extends Fragment {
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (checked.valueAt(i)) {
|
||||
final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
|
||||
final Uri uri = getArguments().getParcelable(EXTRA_URI);
|
||||
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
|
||||
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
|
||||
docs.add(doc);
|
||||
}
|
||||
}
|
||||
@@ -401,14 +416,8 @@ public class DirectoryFragment extends Fragment {
|
||||
}
|
||||
|
||||
private class DocumentsAdapter extends BaseAdapter {
|
||||
private final String mAuthority;
|
||||
|
||||
private Cursor mCursor;
|
||||
|
||||
public DocumentsAdapter(String authority) {
|
||||
mAuthority = authority;
|
||||
}
|
||||
|
||||
public void swapCursor(Cursor cursor) {
|
||||
mCursor = cursor;
|
||||
|
||||
@@ -443,6 +452,8 @@ public class DirectoryFragment extends Fragment {
|
||||
|
||||
final Cursor cursor = getItem(position);
|
||||
|
||||
final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
|
||||
final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
|
||||
final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
|
||||
final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
|
||||
final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
|
||||
@@ -466,7 +477,7 @@ public class DirectoryFragment extends Fragment {
|
||||
}
|
||||
|
||||
if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) {
|
||||
final Uri uri = DocumentsContract.buildDocumentUri(mAuthority, docId);
|
||||
final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
|
||||
final Bitmap cachedResult = thumbs.get(uri);
|
||||
if (cachedResult != null) {
|
||||
icon.setImageBitmap(cachedResult);
|
||||
@@ -477,19 +488,27 @@ public class DirectoryFragment extends Fragment {
|
||||
task.execute(uri);
|
||||
}
|
||||
} else if (docIcon != 0) {
|
||||
icon.setImageDrawable(DocumentInfo.loadIcon(context, mAuthority, docIcon));
|
||||
icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon));
|
||||
} else {
|
||||
icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType));
|
||||
}
|
||||
|
||||
title.setText(docDisplayName);
|
||||
|
||||
icon1.setVisibility(View.GONE);
|
||||
if (docSummary != null) {
|
||||
summary.setText(docSummary);
|
||||
if (mType == TYPE_RECENT_OPEN) {
|
||||
final RootInfo root = roots.getRoot(docAuthority, docRootId);
|
||||
icon1.setVisibility(View.VISIBLE);
|
||||
icon1.setImageDrawable(root.loadIcon(context));
|
||||
summary.setText(root.getDirectoryString());
|
||||
summary.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
summary.setVisibility(View.INVISIBLE);
|
||||
icon1.setVisibility(View.GONE);
|
||||
if (docSummary != null) {
|
||||
summary.setText(docSummary);
|
||||
summary.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
summary.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
if (summaryGrid != null) {
|
||||
|
||||
@@ -48,14 +48,16 @@ class DirectoryResult implements AutoCloseable {
|
||||
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
|
||||
private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
|
||||
|
||||
private final String mRootId;
|
||||
private final Uri mUri;
|
||||
private final int mSortOrder;
|
||||
|
||||
private CancellationSignal mSignal;
|
||||
private DirectoryResult mResult;
|
||||
|
||||
public DirectoryLoader(Context context, Uri uri, int sortOrder) {
|
||||
public DirectoryLoader(Context context, String rootId, Uri uri, int sortOrder) {
|
||||
super(context);
|
||||
mRootId = rootId;
|
||||
mUri = uri;
|
||||
mSortOrder = sortOrder;
|
||||
}
|
||||
@@ -69,12 +71,16 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
|
||||
mSignal = new CancellationSignal();
|
||||
}
|
||||
final DirectoryResult result = new DirectoryResult();
|
||||
final String authority = mUri.getAuthority();
|
||||
try {
|
||||
result.client = getContext()
|
||||
.getContentResolver().acquireUnstableContentProviderClient(mUri.getAuthority());
|
||||
.getContentResolver().acquireUnstableContentProviderClient(authority);
|
||||
final Cursor cursor = result.client.query(
|
||||
mUri, null, null, null, getQuerySortOrder(), mSignal);
|
||||
result.cursor = new SortingCursorWrapper(cursor, mSortOrder);
|
||||
mUri, null, null, null, getQuerySortOrder(mSortOrder), mSignal);
|
||||
final Cursor withRoot = new RootCursorWrapper(mUri.getAuthority(), mRootId, cursor, -1);
|
||||
final Cursor sorted = new SortingCursorWrapper(withRoot, mSortOrder);
|
||||
|
||||
result.cursor = sorted;
|
||||
result.cursor.registerContentObserver(mObserver);
|
||||
} catch (Exception e) {
|
||||
result.exception = e;
|
||||
@@ -149,8 +155,8 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
|
||||
getContext().getContentResolver().unregisterContentObserver(mObserver);
|
||||
}
|
||||
|
||||
private String getQuerySortOrder() {
|
||||
switch (mSortOrder) {
|
||||
public static String getQuerySortOrder(int sortOrder) {
|
||||
switch (sortOrder) {
|
||||
case SORT_ORDER_DISPLAY_NAME:
|
||||
return Document.COLUMN_DISPLAY_NAME + " ASC";
|
||||
case SORT_ORDER_LAST_MODIFIED:
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.DocumentsActivity.TAG;
|
||||
|
||||
import android.content.AsyncTaskLoader;
|
||||
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.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.documentsui.DocumentsActivity.State;
|
||||
import com.android.documentsui.model.RootInfo;
|
||||
import com.google.android.collect.Maps;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.AbstractFuture;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
|
||||
|
||||
public static final int MAX_OUTSTANDING_RECENTS = 2;
|
||||
|
||||
/**
|
||||
* Time to wait for first pass to complete before returning partial results.
|
||||
*/
|
||||
public static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;
|
||||
|
||||
/**
|
||||
* Maximum documents from a single root.
|
||||
*/
|
||||
public static final int MAX_DOCS_FROM_ROOT = 24;
|
||||
|
||||
private static final ExecutorService sExecutor = buildExecutor();
|
||||
|
||||
/**
|
||||
* Create a bounded thread pool for fetching recents; it creates threads as
|
||||
* needed (up to maximum) and reclaims them when finished.
|
||||
*/
|
||||
private static ExecutorService buildExecutor() {
|
||||
// Create a bounded thread pool for fetching recents; it creates
|
||||
// threads as needed (up to maximum) and reclaims them when finished.
|
||||
final ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
MAX_OUTSTANDING_RECENTS, MAX_OUTSTANDING_RECENTS, 10, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<Runnable>());
|
||||
executor.allowCoreThreadTimeOut(true);
|
||||
return executor;
|
||||
}
|
||||
|
||||
private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
|
||||
|
||||
private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;
|
||||
|
||||
private CountDownLatch mFirstPassLatch;
|
||||
private volatile boolean mFirstPassDone;
|
||||
|
||||
private DirectoryResult mResult;
|
||||
|
||||
// TODO: create better transfer of ownership around cursor to ensure its
|
||||
// closed in all edge cases.
|
||||
|
||||
public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable {
|
||||
public final String authority;
|
||||
public final String rootId;
|
||||
|
||||
private Cursor mWithRoot;
|
||||
|
||||
public RecentTask(String authority, String rootId) {
|
||||
this.authority = authority;
|
||||
this.rootId = rootId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isCancelled()) return;
|
||||
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
|
||||
authority);
|
||||
try {
|
||||
final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
|
||||
final Cursor cursor = client.query(
|
||||
uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
|
||||
mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
|
||||
set(mWithRoot);
|
||||
|
||||
mFirstPassLatch.countDown();
|
||||
if (mFirstPassDone) {
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
setException(e);
|
||||
} finally {
|
||||
ContentProviderClient.closeQuietly(client);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IoUtils.closeQuietly(mWithRoot);
|
||||
}
|
||||
}
|
||||
|
||||
public RecentLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirectoryResult loadInBackground() {
|
||||
if (mFirstPassLatch == null) {
|
||||
// First time through we kick off all the recent tasks, and wait
|
||||
// around to see if everyone finishes quickly.
|
||||
|
||||
final RootsCache roots = DocumentsApplication.getRootsCache(getContext());
|
||||
for (RootInfo root : roots.getRoots()) {
|
||||
if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
|
||||
final RecentTask task = new RecentTask(root.authority, root.rootId);
|
||||
mTasks.put(root, task);
|
||||
}
|
||||
}
|
||||
|
||||
mFirstPassLatch = new CountDownLatch(mTasks.size());
|
||||
for (RecentTask task : mTasks.values()) {
|
||||
sExecutor.execute(task);
|
||||
}
|
||||
|
||||
try {
|
||||
mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
|
||||
mFirstPassDone = true;
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all finished tasks
|
||||
List<Cursor> cursors = Lists.newArrayList();
|
||||
for (RecentTask task : mTasks.values()) {
|
||||
if (task.isDone()) {
|
||||
try {
|
||||
cursors.add(task.get());
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
Log.w(TAG, "Failed to load " + task.authority + ", " + task.rootId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final DirectoryResult result = new DirectoryResult();
|
||||
if (cursors.size() > 0) {
|
||||
final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
|
||||
final SortingCursorWrapper sorted = new SortingCursorWrapper(
|
||||
merged, State.SORT_ORDER_LAST_MODIFIED) {
|
||||
@Override
|
||||
public void close() {
|
||||
// Ignored, since we manage cursor lifecycle internally
|
||||
}
|
||||
};
|
||||
result.cursor = sorted;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelLoadInBackground() {
|
||||
super.cancelLoadInBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(DirectoryResult result) {
|
||||
if (isReset()) {
|
||||
IoUtils.closeQuietly(result);
|
||||
return;
|
||||
}
|
||||
DirectoryResult oldResult = mResult;
|
||||
mResult = result;
|
||||
|
||||
if (isStarted()) {
|
||||
super.deliverResult(result);
|
||||
}
|
||||
|
||||
if (oldResult != null && oldResult != result) {
|
||||
IoUtils.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) {
|
||||
IoUtils.closeQuietly(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
|
||||
// Ensure the loader is stopped
|
||||
onStopLoading();
|
||||
|
||||
for (RecentTask task : mTasks.values()) {
|
||||
IoUtils.closeQuietly(task);
|
||||
}
|
||||
|
||||
IoUtils.closeQuietly(mResult);
|
||||
mResult = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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 android.database.AbstractCursor;
|
||||
import android.database.Cursor;
|
||||
|
||||
/**
|
||||
* Cursor wrapper that adds columns to identify which root a document came from.
|
||||
*/
|
||||
public class RootCursorWrapper extends AbstractCursor {
|
||||
private final String mAuthority;
|
||||
private final String mRootId;
|
||||
|
||||
private final Cursor mCursor;
|
||||
private final int mCount;
|
||||
|
||||
private final String[] mColumnNames;
|
||||
|
||||
private final int mAuthorityIndex;
|
||||
private final int mRootIdIndex;
|
||||
|
||||
public static final String COLUMN_AUTHORITY = "android:authority";
|
||||
public static final String COLUMN_ROOT_ID = "android:rootId";
|
||||
|
||||
public RootCursorWrapper(String authority, String rootId, Cursor cursor, int maxCount) {
|
||||
mAuthority = authority;
|
||||
mRootId = rootId;
|
||||
mCursor = cursor;
|
||||
|
||||
final int count = cursor.getCount();
|
||||
if (maxCount > 0 && count > maxCount) {
|
||||
mCount = maxCount;
|
||||
} else {
|
||||
mCount = count;
|
||||
}
|
||||
|
||||
if (cursor.getColumnIndex(COLUMN_AUTHORITY) != -1
|
||||
|| cursor.getColumnIndex(COLUMN_ROOT_ID) != -1) {
|
||||
throw new IllegalArgumentException("Cursor contains internal columns!");
|
||||
}
|
||||
final String[] before = cursor.getColumnNames();
|
||||
mColumnNames = new String[before.length + 2];
|
||||
System.arraycopy(before, 0, mColumnNames, 0, before.length);
|
||||
mAuthorityIndex = before.length;
|
||||
mRootIdIndex = before.length + 1;
|
||||
mColumnNames[mAuthorityIndex] = COLUMN_AUTHORITY;
|
||||
mColumnNames[mRootIdIndex] = COLUMN_ROOT_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
mCursor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(int oldPosition, int newPosition) {
|
||||
return mCursor.moveToPosition(newPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getColumnNames() {
|
||||
return mColumnNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int column) {
|
||||
return mCursor.getDouble(column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(int column) {
|
||||
return mCursor.getFloat(column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(int column) {
|
||||
return mCursor.getInt(column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(int column) {
|
||||
return mCursor.getLong(column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getShort(int column) {
|
||||
return mCursor.getShort(column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int column) {
|
||||
if (column == mAuthorityIndex) {
|
||||
return mAuthority;
|
||||
} else if (column == mRootIdIndex) {
|
||||
return mRootId;
|
||||
} else {
|
||||
return mCursor.getString(column);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType(int column) {
|
||||
return mCursor.getType(column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(int column) {
|
||||
return mCursor.isNull(column);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class RootsCache {
|
||||
// TODO: cache roots in local provider to avoid spinning up backends
|
||||
// TODO: root updates should trigger UI refresh
|
||||
|
||||
private static final boolean RECENTS_ENABLED = false;
|
||||
private static final boolean RECENTS_ENABLED = true;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
@@ -125,6 +125,16 @@ public class RootsCache {
|
||||
return null;
|
||||
}
|
||||
|
||||
@GuardedBy("ActivityThread")
|
||||
public RootInfo getRoot(String authority, String rootId) {
|
||||
for (RootInfo root : mRoots) {
|
||||
if (Objects.equal(root.authority, authority) && Objects.equal(root.rootId, rootId)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GuardedBy("ActivityThread")
|
||||
public RootInfo getRecentsRoot() {
|
||||
return mRecentsRoot;
|
||||
|
||||
@@ -54,11 +54,6 @@ public class SortingCursorWrapper extends AbstractCursor {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
final int mimeTypeIndex = cursor.getColumnIndex(Document.COLUMN_MIME_TYPE);
|
||||
final int displayNameIndex = cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME);
|
||||
final int lastModifiedIndex = cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED);
|
||||
final int sizeIndex = cursor.getColumnIndex(Document.COLUMN_SIZE);
|
||||
|
||||
cursor.moveToPosition(-1);
|
||||
for (int i = 0; i < count; i++) {
|
||||
cursor.moveToNext();
|
||||
@@ -66,8 +61,10 @@ public class SortingCursorWrapper extends AbstractCursor {
|
||||
|
||||
switch (sortOrder) {
|
||||
case SORT_ORDER_DISPLAY_NAME:
|
||||
final String mimeType = cursor.getString(mimeTypeIndex);
|
||||
final String displayName = cursor.getString(displayNameIndex);
|
||||
final String mimeType = cursor.getString(
|
||||
cursor.getColumnIndex(Document.COLUMN_MIME_TYPE));
|
||||
final String displayName = cursor.getString(
|
||||
cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
mValueString[i] = '\001' + displayName;
|
||||
} else {
|
||||
@@ -75,10 +72,11 @@ public class SortingCursorWrapper extends AbstractCursor {
|
||||
}
|
||||
break;
|
||||
case SORT_ORDER_LAST_MODIFIED:
|
||||
mValueLong[i] = cursor.getLong(lastModifiedIndex);
|
||||
mValueLong[i] = cursor.getLong(
|
||||
cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED));
|
||||
break;
|
||||
case SORT_ORDER_SIZE:
|
||||
mValueLong[i] = cursor.getLong(sizeIndex);
|
||||
mValueLong[i] = cursor.getLong(cursor.getColumnIndex(Document.COLUMN_SIZE));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
|
||||
import com.android.documentsui.RecentsProvider;
|
||||
import com.android.documentsui.RootCursorWrapper;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
@@ -101,9 +102,9 @@ public class DocumentInfo implements Durable {
|
||||
out.writeInt(icon);
|
||||
}
|
||||
|
||||
public static DocumentInfo fromDirectoryCursor(Uri parent, Cursor cursor) {
|
||||
public static DocumentInfo fromDirectoryCursor(Cursor cursor) {
|
||||
final DocumentInfo doc = new DocumentInfo();
|
||||
final String authority = parent.getAuthority();
|
||||
final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
|
||||
final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
|
||||
doc.uri = DocumentsContract.buildDocumentUri(authority, docId);
|
||||
doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
|
||||
|
||||
@@ -25,6 +25,8 @@ import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Representation of a {@link Root}.
|
||||
*/
|
||||
@@ -56,4 +58,23 @@ public class RootInfo {
|
||||
public Drawable loadIcon(Context context) {
|
||||
return DocumentInfo.loadIcon(context, authority, icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof RootInfo) {
|
||||
final RootInfo root = (RootInfo) o;
|
||||
return Objects.equals(authority, root.authority) && Objects.equals(rootId, root.rootId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(authority, rootId);
|
||||
}
|
||||
|
||||
public String getDirectoryString() {
|
||||
return (summary != null) ? summary : title;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user