Loading Android.mk +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading src/com/android/documentsui/DirectoryFragment.java +53 −34 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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); Loading Loading @@ -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>() { Loading @@ -170,15 +176,26 @@ public class DirectoryFragment extends Fragment { public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { final State state = getDisplayState(DirectoryFragment.this); 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); Uri contentsUri; if (mType == TYPE_NORMAL) { contentsUri = DocumentsContract.buildChildDocumentsUri( uri.getAuthority(), DocumentsContract.getDocumentId(uri)); } else { contentsUri = uri; } 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); return new DirectoryLoader(context, contentsUri, state.sortOrder); } } @Override Loading Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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; Loading Loading @@ -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); Loading @@ -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); Loading @@ -477,13 +488,20 @@ 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); 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 { icon1.setVisibility(View.GONE); if (docSummary != null) { summary.setText(docSummary); Loading @@ -491,6 +509,7 @@ public class DirectoryFragment extends Fragment { } else { summary.setVisibility(View.INVISIBLE); } } if (summaryGrid != null) { summaryGrid.setVisibility( Loading src/com/android/documentsui/DirectoryLoader.java +12 −6 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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; Loading Loading @@ -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: Loading src/com/android/documentsui/RecentLoader.java 0 → 100644 +253 −0 Original line number Diff line number Diff line /* * 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; } } src/com/android/documentsui/RootCursorWrapper.java 0 → 100644 +132 −0 Original line number Diff line number Diff line /* * 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); } } Loading
Android.mk +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
src/com/android/documentsui/DirectoryFragment.java +53 −34 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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); Loading Loading @@ -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>() { Loading @@ -170,15 +176,26 @@ public class DirectoryFragment extends Fragment { public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { final State state = getDisplayState(DirectoryFragment.this); 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); Uri contentsUri; if (mType == TYPE_NORMAL) { contentsUri = DocumentsContract.buildChildDocumentsUri( uri.getAuthority(), DocumentsContract.getDocumentId(uri)); } else { contentsUri = uri; } 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); return new DirectoryLoader(context, contentsUri, state.sortOrder); } } @Override Loading Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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; Loading Loading @@ -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); Loading @@ -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); Loading @@ -477,13 +488,20 @@ 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); 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 { icon1.setVisibility(View.GONE); if (docSummary != null) { summary.setText(docSummary); Loading @@ -491,6 +509,7 @@ public class DirectoryFragment extends Fragment { } else { summary.setVisibility(View.INVISIBLE); } } if (summaryGrid != null) { summaryGrid.setVisibility( Loading
src/com/android/documentsui/DirectoryLoader.java +12 −6 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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; Loading Loading @@ -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: Loading
src/com/android/documentsui/RecentLoader.java 0 → 100644 +253 −0 Original line number Diff line number Diff line /* * 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; } }
src/com/android/documentsui/RootCursorWrapper.java 0 → 100644 +132 −0 Original line number Diff line number Diff line /* * 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); } }