Merge "Remember mode and sort on per-directory basis." into klp-dev

This commit is contained in:
Jeff Sharkey
2013-09-09 17:04:46 +00:00
committed by Android (Google) Code Review
15 changed files with 701 additions and 290 deletions

View File

@@ -20796,6 +20796,7 @@ package android.provider {
field public static final java.lang.String COLUMN_SIZE = "_size";
field public static final java.lang.String COLUMN_SUMMARY = "summary";
field public static final int FLAG_DIR_PREFERS_GRID = 32; // 0x20
field public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 64; // 0x40
field public static final int FLAG_DIR_SUPPORTS_CREATE = 8; // 0x8
field public static final int FLAG_DIR_SUPPORTS_SEARCH = 16; // 0x10
field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4

View File

@@ -251,6 +251,15 @@ public final class DocumentsContract {
* @see #COLUMN_FLAGS
*/
public static final int FLAG_DIR_PREFERS_GRID = 1 << 5;
/**
* Flag indicating that a directory prefers its contents be sorted by
* {@link #COLUMN_LAST_MODIFIED}. Only valid when
* {@link #COLUMN_MIME_TYPE} is {@link #MIME_TYPE_DIR}.
*
* @see #COLUMN_FLAGS
*/
public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 6;
}
/**
@@ -292,9 +301,6 @@ public final class DocumentsContract {
* @see #FLAG_LOCAL_ONLY
* @see #FLAG_SUPPORTS_CREATE
* @see #FLAG_ADVANCED
* @see #FLAG_PROVIDES_AUDIO
* @see #FLAG_PROVIDES_IMAGES
* @see #FLAG_PROVIDES_VIDEO
*/
public static final String COLUMN_FLAGS = "flags";

View File

@@ -70,7 +70,7 @@ public class CreateDirectoryFragment extends DialogFragment {
try {
final Uri childUri = DocumentsContract.createDocument(
resolver, cwd.uri, Document.MIME_TYPE_DIR, displayName);
resolver, cwd.derivedUri, Document.MIME_TYPE_DIR, displayName);
// Navigate into newly created child
final DocumentInfo childDoc = DocumentInfo.fromUri(resolver, childUri);

View File

@@ -20,6 +20,8 @@ import static com.android.documentsui.DocumentsActivity.TAG;
import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
@@ -91,43 +93,42 @@ public class DirectoryFragment extends Fragment {
private int mType = TYPE_NORMAL;
private int mLastMode = MODE_UNKNOWN;
private int mLastSortOrder = SORT_ORDER_UNKNOWN;
private Point mThumbSize;
private DocumentsAdapter mAdapter;
private LoaderCallbacks<DirectoryResult> mCallbacks;
private static final String EXTRA_TYPE = "type";
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_ROOT = "root";
private static final String EXTRA_DOC = "doc";
private static final String EXTRA_QUERY = "query";
private static AtomicInteger sLoaderId = new AtomicInteger(4000);
private int mLastSortOrder = -1;
private final int mLoaderId = sLoaderId.incrementAndGet();
public static void showNormal(FragmentManager fm, Uri uri) {
show(fm, TYPE_NORMAL, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), null);
public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc) {
show(fm, TYPE_NORMAL, root, doc, null);
}
public static void showSearch(FragmentManager fm, Uri uri, String query) {
show(fm, TYPE_SEARCH, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri),
query);
public static void showSearch(
FragmentManager fm, RootInfo root, DocumentInfo doc, String query) {
show(fm, TYPE_SEARCH, root, doc, query);
}
public static void showRecentsOpen(FragmentManager fm) {
show(fm, TYPE_RECENT_OPEN, null, null, null, null);
show(fm, TYPE_RECENT_OPEN, null, null, null);
}
private static void show(FragmentManager fm, int type, String authority, String rootId,
String docId, String query) {
private static void show(
FragmentManager fm, int type, RootInfo root, DocumentInfo doc, String query) {
final Bundle args = new Bundle();
args.putInt(EXTRA_TYPE, type);
args.putString(EXTRA_AUTHORITY, authority);
args.putString(EXTRA_ROOT_ID, rootId);
args.putString(EXTRA_DOC_ID, docId);
args.putParcelable(EXTRA_ROOT, root);
args.putParcelable(EXTRA_DOC, doc);
args.putString(EXTRA_QUERY, query);
final DirectoryFragment fragment = new DirectoryFragment();
@@ -167,6 +168,7 @@ public class DirectoryFragment extends Fragment {
super.onActivityCreated(savedInstanceState);
final Context context = getActivity();
final State state = getDisplayState(DirectoryFragment.this);
mAdapter = new DocumentsAdapter();
mType = getArguments().getInt(EXTRA_TYPE);
@@ -174,35 +176,48 @@ public class DirectoryFragment extends Fragment {
mCallbacks = new LoaderCallbacks<DirectoryResult>() {
@Override
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 RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
final String query = getArguments().getString(EXTRA_QUERY);
Uri contentsUri;
switch (mType) {
case TYPE_NORMAL:
contentsUri = DocumentsContract.buildChildDocumentsUri(authority, docId);
return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
contentsUri = DocumentsContract.buildChildDocumentsUri(
doc.authority, doc.documentId);
return new DirectoryLoader(context, root, doc, contentsUri);
case TYPE_SEARCH:
contentsUri = DocumentsContract.buildSearchDocumentsUri(
authority, docId, query);
return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
doc.authority, doc.documentId, query);
return new DirectoryLoader(context, root, doc, contentsUri);
case TYPE_RECENT_OPEN:
final RootsCache roots = DocumentsApplication.getRootsCache(context);
final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
return new RecentLoader(context, matchingRoots);
return new RecentLoader(context, matchingRoots, state.acceptMimes);
default:
throw new IllegalStateException("Unknown type " + mType);
}
}
@Override
public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
if (!isAdded()) return;
mAdapter.swapCursor(result.cursor);
// Push latest state up to UI
// TODO: if mode change was racing with us, don't overwrite it
state.mode = result.mode;
state.sortOrder = result.sortOrder;
((DocumentsActivity) context).onStateChanged();
updateDisplayState();
if (mLastSortOrder != result.sortOrder) {
mLastSortOrder = result.sortOrder;
mListView.smoothScrollToPosition(0);
mGridView.smoothScrollToPosition(0);
}
}
@Override
@@ -211,6 +226,9 @@ public class DirectoryFragment extends Fragment {
}
};
// Kick off loader at least once
getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
updateDisplayState();
}
@@ -220,22 +238,27 @@ public class DirectoryFragment extends Fragment {
updateDisplayState();
}
public void updateDisplayState() {
public void onUserSortOrderChanged() {
// User change always triggers reload
getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
}
public void onUserModeChanged() {
// Mode change is just display; no need to reload
updateDisplayState();
}
private void updateDisplayState() {
final State state = getDisplayState(this);
if (mLastSortOrder != state.sortOrder) {
getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
mLastSortOrder = state.sortOrder;
}
mFilter = new MimePredicate(state.acceptMimes);
mListView.smoothScrollToPosition(0);
mGridView.smoothScrollToPosition(0);
if (mLastMode == state.mode) return;
mLastMode = state.mode;
mListView.setVisibility(state.mode == MODE_LIST ? View.VISIBLE : View.GONE);
mGridView.setVisibility(state.mode == MODE_GRID ? View.VISIBLE : View.GONE);
mFilter = new MimePredicate(state.acceptMimes);
final int choiceMode;
if (state.allowMultiple) {
choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
@@ -254,14 +277,14 @@ public class DirectoryFragment extends Fragment {
mGridView.setChoiceMode(choiceMode);
mCurrentView = mGridView;
} else if (state.mode == MODE_LIST) {
thumbSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
mGridView.setAdapter(null);
mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
mListView.setAdapter(mAdapter);
mListView.setChoiceMode(choiceMode);
mCurrentView = mListView;
} else {
throw new IllegalStateException();
throw new IllegalStateException("Unknown state " + state.mode);
}
mThumbSize = new Point(thumbSize, thumbSize);
@@ -366,7 +389,7 @@ public class DirectoryFragment extends Fragment {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setType(doc.mimeType);
intent.putExtra(Intent.EXTRA_STREAM, doc.uri);
intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
} else if (docs.size() > 1) {
intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
@@ -377,7 +400,7 @@ public class DirectoryFragment extends Fragment {
final ArrayList<Uri> uris = Lists.newArrayList();
for (DocumentInfo doc : docs) {
mimeTypes.add(doc.mimeType);
uris.add(doc.uri);
uris.add(doc.derivedUri);
}
intent.setType(findCommonMimeType(mimeTypes));
@@ -403,7 +426,7 @@ public class DirectoryFragment extends Fragment {
continue;
}
if (!DocumentsContract.deleteDocument(resolver, doc.uri)) {
if (!DocumentsContract.deleteDocument(resolver, doc.derivedUri)) {
Log.w(TAG, "Failed to delete " + doc);
hadTrouble = true;
}

View File

@@ -16,18 +16,29 @@
package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME;
import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE;
import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import android.content.AsyncTaskLoader;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import com.android.documentsui.DocumentsActivity.State;
import com.android.documentsui.RecentsProvider.StateColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import libcore.io.IoUtils;
@@ -36,6 +47,9 @@ class DirectoryResult implements AutoCloseable {
Cursor cursor;
Exception exception;
int mode = MODE_UNKNOWN;
int sortOrder = SORT_ORDER_UNKNOWN;
@Override
public void close() {
IoUtils.closeQuietly(cursor);
@@ -48,18 +62,18 @@ class DirectoryResult implements AutoCloseable {
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
private final String mRootId;
private final RootInfo mRoot;
private final DocumentInfo mDoc;
private final Uri mUri;
private final int mSortOrder;
private CancellationSignal mSignal;
private DirectoryResult mResult;
public DirectoryLoader(Context context, String rootId, Uri uri, int sortOrder) {
public DirectoryLoader(Context context, RootInfo root, DocumentInfo doc, Uri uri) {
super(context);
mRootId = rootId;
mRoot = root;
mDoc = doc;
mUri = uri;
mSortOrder = sortOrder;
}
@Override
@@ -70,20 +84,65 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
}
mSignal = new CancellationSignal();
}
final DirectoryResult result = new DirectoryResult();
final ContentResolver resolver = getContext().getContentResolver();
final String authority = mUri.getAuthority();
final DirectoryResult result = new DirectoryResult();
int userMode = State.MODE_UNKNOWN;
int userSortOrder = State.SORT_ORDER_UNKNOWN;
// Pick up any custom modes requested by user
Cursor cursor = null;
try {
result.client = getContext()
.getContentResolver().acquireUnstableContentProviderClient(authority);
final Cursor cursor = result.client.query(
mUri, null, null, null, getQuerySortOrder(mSortOrder), mSignal);
final Uri stateUri = RecentsProvider.buildState(
mRoot.authority, mRoot.rootId, mDoc.documentId);
cursor = resolver.query(stateUri, null, null, null, null);
if (cursor.moveToFirst()) {
userMode = getCursorInt(cursor, StateColumns.MODE);
userSortOrder = getCursorInt(cursor, StateColumns.SORT_ORDER);
}
} finally {
IoUtils.closeQuietly(cursor);
}
if (userMode != State.MODE_UNKNOWN) {
result.mode = userMode;
} else {
if ((mDoc.flags & Document.FLAG_DIR_PREFERS_GRID) != 0) {
result.mode = State.MODE_GRID;
} else {
result.mode = State.MODE_LIST;
}
}
if (userSortOrder != State.SORT_ORDER_UNKNOWN) {
result.sortOrder = userSortOrder;
} else {
if ((mDoc.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0) {
result.sortOrder = State.SORT_ORDER_LAST_MODIFIED;
} else {
result.sortOrder = State.SORT_ORDER_DISPLAY_NAME;
}
}
Log.d(TAG, "userMode=" + userMode + ", userSortOrder=" + userSortOrder + " --> mode="
+ result.mode + ", sortOrder=" + result.sortOrder);
try {
result.client = resolver.acquireUnstableContentProviderClient(authority);
cursor = result.client.query(
mUri, null, null, null, getQuerySortOrder(result.sortOrder), mSignal);
cursor.registerContentObserver(mObserver);
final Cursor withRoot = new RootCursorWrapper(mUri.getAuthority(), mRootId, cursor, -1);
final Cursor sorted = new SortingCursorWrapper(withRoot, mSortOrder);
final Cursor withRoot = new RootCursorWrapper(
mUri.getAuthority(), mRoot.rootId, cursor, -1);
final Cursor sorted = new SortingCursorWrapper(withRoot, result.sortOrder);
result.cursor = sorted;
} catch (Exception e) {
Log.d(TAG, "Failed to query", e);
result.exception = e;
ContentProviderClient.closeQuietly(result.client);
} finally {
@@ -91,6 +150,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
mSignal = null;
}
}
return result;
}

View File

@@ -60,6 +60,9 @@ import android.widget.SearchView.OnQueryTextListener;
import android.widget.TextView;
import android.widget.Toast;
import com.android.documentsui.RecentsProvider.RecentColumns;
import com.android.documentsui.RecentsProvider.ResumeColumns;
import com.android.documentsui.RecentsProvider.StateColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
@@ -191,7 +194,7 @@ public class DocumentsActivity extends Activity {
try {
if (cursor.moveToFirst()) {
final byte[] rawStack = cursor.getBlob(
cursor.getColumnIndex(RecentsProvider.COL_PATH));
cursor.getColumnIndex(ResumeColumns.STACK));
DurableUtils.readFromArray(rawStack, mState.stack);
}
} catch (IOException e) {
@@ -204,7 +207,7 @@ public class DocumentsActivity extends Activity {
final RootInfo root = getCurrentRoot();
final List<RootInfo> matchingRoots = mRoots.getMatchingRoots(mState);
if (!matchingRoots.contains(root)) {
mState.stack.clear();
mState.stack.reset();
}
// Only open drawer when showing recents
@@ -343,11 +346,16 @@ public class DocumentsActivity extends Activity {
final MenuItem list = menu.findItem(R.id.menu_list);
final MenuItem settings = menu.findItem(R.id.menu_settings);
grid.setVisible(mState.mode != MODE_GRID);
list.setVisible(mState.mode != MODE_LIST);
if (cwd != null) {
sort.setVisible(true);
grid.setVisible(mState.mode != MODE_GRID);
list.setVisible(mState.mode != MODE_LIST);
} else {
sort.setVisible(false);
grid.setVisible(false);
list.setVisible(false);
}
// No sorting in recents
sort.setVisible(cwd != null);
// Only sort by size when visible
sortSize.setVisible(mState.showSize);
@@ -392,28 +400,19 @@ public class DocumentsActivity extends Activity {
} else if (id == R.id.menu_search) {
return false;
} else if (id == R.id.menu_sort_name) {
mState.sortOrder = State.SORT_ORDER_DISPLAY_NAME;
updateDisplayState();
setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
return true;
} else if (id == R.id.menu_sort_date) {
mState.sortOrder = State.SORT_ORDER_LAST_MODIFIED;
updateDisplayState();
setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
return true;
} else if (id == R.id.menu_sort_size) {
mState.sortOrder = State.SORT_ORDER_SIZE;
updateDisplayState();
setUserSortOrder(State.SORT_ORDER_SIZE);
return true;
} else if (id == R.id.menu_grid) {
// TODO: persist explicit user mode for cwd
mState.mode = MODE_GRID;
updateDisplayState();
invalidateOptionsMenu();
setUserMode(State.MODE_GRID);
return true;
} else if (id == R.id.menu_list) {
// TODO: persist explicit user mode for cwd
mState.mode = MODE_LIST;
updateDisplayState();
invalidateOptionsMenu();
setUserMode(State.MODE_LIST);
return true;
} else if (id == R.id.menu_settings) {
startActivity(new Intent(this, SettingsActivity.class));
@@ -423,6 +422,51 @@ public class DocumentsActivity extends Activity {
}
}
/**
* Update UI to reflect internal state changes not from user.
*/
public void onStateChanged() {
invalidateOptionsMenu();
}
/**
* Set state sort order based on explicit user action.
*/
private void setUserSortOrder(int sortOrder) {
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
// TODO: persist async, then trigger rebind
final Uri stateUri = RecentsProvider.buildState(
root.authority, root.rootId, cwd.documentId);
final ContentValues values = new ContentValues();
values.put(StateColumns.SORT_ORDER, sortOrder);
getContentResolver().insert(stateUri, values);
DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
onStateChanged();
}
/**
* Set state mode based on explicit user action.
*/
private void setUserMode(int mode) {
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
// TODO: persist async, then trigger rebind
final Uri stateUri = RecentsProvider.buildState(
root.authority, root.rootId, cwd.documentId);
final ContentValues values = new ContentValues();
values.put(StateColumns.MODE, mode);
getContentResolver().insert(stateUri, values);
mState.mode = mode;
DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
onStateChanged();
}
@Override
public void onBackPressed() {
final int size = mState.stack.size();
@@ -528,8 +572,8 @@ public class DocumentsActivity extends Activity {
};
public RootInfo getCurrentRoot() {
if (mState.stack.size() > 0) {
return mState.stack.getRoot(mRoots);
if (mState.stack.root != null) {
return mState.stack.root;
} else {
return mRoots.getRecentsRoot();
}
@@ -545,6 +589,7 @@ public class DocumentsActivity extends Activity {
private void onCurrentDirectoryChanged() {
final FragmentManager fm = getFragmentManager();
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
if (cwd == null) {
@@ -557,10 +602,10 @@ public class DocumentsActivity extends Activity {
} else {
if (mState.currentSearch != null) {
// Ongoing search
DirectoryFragment.showSearch(fm, cwd.uri, mState.currentSearch);
DirectoryFragment.showSearch(fm, root, cwd, mState.currentSearch);
} else {
// Normal boring directory
DirectoryFragment.showNormal(fm, cwd.uri);
DirectoryFragment.showNormal(fm, root, cwd);
}
}
@@ -582,11 +627,6 @@ public class DocumentsActivity extends Activity {
dumpStack();
}
private void updateDisplayState() {
// TODO: handle multiple directory stacks on tablets
DirectoryFragment.get(getFragmentManager()).updateDisplayState();
}
public void onStackPicked(DocumentStack stack) {
mState.stack = stack;
onCurrentDirectoryChanged();
@@ -594,6 +634,7 @@ public class DocumentsActivity extends Activity {
public void onRootPicked(RootInfo root, boolean closeDrawer) {
// Clear entire backstack and start in new root
mState.stack.root = root;
mState.stack.clear();
if (!mRoots.isRecentsRoot(root)) {
@@ -633,7 +674,7 @@ public class DocumentsActivity extends Activity {
onCurrentDirectoryChanged();
} else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
// Explicit file picked, return
onFinished(doc.uri);
onFinished(doc.derivedUri);
} else if (mState.action == ACTION_CREATE) {
// Replace selected file
SaveFragment.get(fm).setReplaceTarget(doc);
@@ -641,7 +682,7 @@ public class DocumentsActivity extends Activity {
// First try managing the document; we expect manager to filter
// based on authority, so we don't grant.
final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
manage.setData(doc.uri);
manage.setData(doc.derivedUri);
try {
startActivity(manage);
@@ -649,7 +690,7 @@ public class DocumentsActivity extends Activity {
// Fall back to viewing
final Intent view = new Intent(Intent.ACTION_VIEW);
view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
view.setData(doc.uri);
view.setData(doc.derivedUri);
try {
startActivity(view);
@@ -665,22 +706,21 @@ public class DocumentsActivity extends Activity {
final int size = docs.size();
final Uri[] uris = new Uri[size];
for (int i = 0; i < size; i++) {
uris[i] = docs.get(i).uri;
uris[i] = docs.get(i).derivedUri;
}
onFinished(uris);
}
}
public void onSaveRequested(DocumentInfo replaceTarget) {
onFinished(replaceTarget.uri);
onFinished(replaceTarget.derivedUri);
}
public void onSaveRequested(String mimeType, String displayName) {
final DocumentInfo cwd = getCurrentDirectory();
final String authority = cwd.uri.getAuthority();
final Uri childUri = DocumentsContract.createDocument(
getContentResolver(), cwd.uri, mimeType, displayName);
getContentResolver(), cwd.derivedUri, mimeType, displayName);
if (childUri != null) {
onFinished(childUri);
} else {
@@ -698,22 +738,14 @@ public class DocumentsActivity extends Activity {
if (mState.action == ACTION_CREATE) {
// Remember stack for last create
values.clear();
values.put(RecentsProvider.COL_PATH, rawStack);
resolver.insert(RecentsProvider.buildRecentCreate(), values);
} else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
// Remember opened items
for (Uri uri : uris) {
values.clear();
values.put(RecentsProvider.COL_URI, uri.toString());
resolver.insert(RecentsProvider.buildRecentOpen(), values);
}
values.put(RecentColumns.STACK, rawStack);
resolver.insert(RecentsProvider.buildRecent(), values);
}
// Remember location for next app launch
final String packageName = getCallingPackage();
values.clear();
values.put(RecentsProvider.COL_PATH, rawStack);
values.put(ResumeColumns.STACK, rawStack);
resolver.insert(RecentsProvider.buildResume(packageName), values);
final Intent intent = new Intent();
@@ -760,12 +792,14 @@ public class DocumentsActivity extends Activity {
public static final int ACTION_GET_CONTENT = 3;
public static final int ACTION_MANAGE = 4;
public static final int MODE_LIST = 0;
public static final int MODE_GRID = 1;
public static final int MODE_UNKNOWN = 0;
public static final int MODE_LIST = 1;
public static final int MODE_GRID = 2;
public static final int SORT_ORDER_DISPLAY_NAME = 0;
public static final int SORT_ORDER_LAST_MODIFIED = 1;
public static final int SORT_ORDER_SIZE = 2;
public static final int SORT_ORDER_UNKNOWN = 0;
public static final int SORT_ORDER_DISPLAY_NAME = 1;
public static final int SORT_ORDER_LAST_MODIFIED = 2;
public static final int SORT_ORDER_SIZE = 3;
@Override
public int describeContents() {
@@ -811,9 +845,10 @@ public class DocumentsActivity extends Activity {
}
private void dumpStack() {
Log.d(TAG, "Current stack:");
Log.d(TAG, "Current stack: ");
Log.d(TAG, " * " + mState.stack.root);
for (DocumentInfo doc : mState.stack) {
Log.d(TAG, "--> " + doc);
Log.d(TAG, " +-- " + doc);
}
}

View File

@@ -0,0 +1,119 @@
/*
* 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.database.AbstractCursor;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.DocumentsContract.Document;
import android.util.Log;
/**
* Cursor wrapper that filters MIME types not matching given list.
*/
public class FilteringCursorWrapper extends AbstractCursor {
private final Cursor mCursor;
private final int[] mPosition;
private int mCount;
public FilteringCursorWrapper(Cursor cursor, String[] acceptMimes) {
mCursor = cursor;
final int count = cursor.getCount();
mPosition = new int[count];
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
final String mimeType = cursor.getString(
cursor.getColumnIndex(Document.COLUMN_MIME_TYPE));
if (MimePredicate.mimeMatches(acceptMimes, mimeType)) {
mPosition[mCount++] = cursor.getPosition();
}
}
Log.d(TAG, "Before filtering " + cursor.getCount() + ", after " + mCount);
}
@Override
public Bundle getExtras() {
return mCursor.getExtras();
}
@Override
public void close() {
super.close();
mCursor.close();
}
@Override
public boolean onMove(int oldPosition, int newPosition) {
return mCursor.moveToPosition(mPosition[newPosition]);
}
@Override
public String[] getColumnNames() {
return mCursor.getColumnNames();
}
@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) {
return mCursor.getString(column);
}
@Override
public int getType(int column) {
return mCursor.getType(column);
}
@Override
public boolean isNull(int column) {
return mCursor.isNull(column);
}
}

View File

@@ -49,6 +49,18 @@ public class MimePredicate implements Predicate<DocumentInfo> {
return false;
}
public static boolean mimeMatches(String filter, String[] tests) {
if (tests == null) {
return true;
}
for (String test : tests) {
if (mimeMatches(filter, test)) {
return true;
}
}
return false;
}
public static boolean mimeMatches(String[] filters, String test) {
if (filters == null) {
return true;

View File

@@ -17,6 +17,9 @@
package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
import android.content.AsyncTaskLoader;
import android.content.ContentProviderClient;
@@ -79,6 +82,7 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
}
private final List<RootInfo> mRoots;
private final String[] mAcceptMimes;
private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
@@ -135,9 +139,10 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
}
}
public RecentLoader(Context context, List<RootInfo> roots) {
public RecentLoader(Context context, List<RootInfo> roots, String[] acceptMimes) {
super(context);
mRoots = roots;
mAcceptMimes = acceptMimes;
}
@Override
@@ -171,7 +176,15 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
for (RecentTask task : mTasks.values()) {
if (task.isDone()) {
try {
cursors.add(task.get());
final Cursor cursor = task.get();
final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
cursor, mAcceptMimes) {
@Override
public void close() {
// Ignored, since we manage cursor lifecycle internally
}
};
cursors.add(filtered);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
@@ -181,15 +194,14 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
}
final DirectoryResult result = new DirectoryResult();
final boolean acceptImages = MimePredicate.mimeMatches("image/*", mAcceptMimes);
result.mode = acceptImages ? MODE_GRID : MODE_LIST;
result.sortOrder = SORT_ORDER_LAST_MODIFIED;
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
}
};
final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder);
result.cursor = sorted;
}
return result;

View File

@@ -41,8 +41,8 @@ import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.android.documentsui.RecentsProvider.RecentColumns;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
@@ -128,7 +128,7 @@ public class RecentsCreateFragment extends Fragment {
public static class RecentsCreateLoader extends UriDerivativeLoader<Uri, List<DocumentStack>> {
public RecentsCreateLoader(Context context) {
super(context, RecentsProvider.buildRecentCreate());
super(context, RecentsProvider.buildRecent());
}
@Override
@@ -137,14 +137,14 @@ public class RecentsCreateFragment extends Fragment {
final ContentResolver resolver = getContext().getContentResolver();
final Cursor cursor = resolver.query(
uri, null, null, null, RecentsProvider.COL_TIMESTAMP + " DESC", signal);
uri, null, null, null, RecentColumns.TIMESTAMP + " DESC", signal);
try {
while (cursor != null && cursor.moveToNext()) {
final byte[] raw = cursor.getBlob(
cursor.getColumnIndex(RecentsProvider.COL_PATH));
final byte[] rawStack = cursor.getBlob(
cursor.getColumnIndex(RecentColumns.STACK));
try {
final DocumentStack stack = new DocumentStack();
stack.read(new DataInputStream(new ByteArrayInputStream(raw)));
stack.read(new DataInputStream(new ByteArrayInputStream(rawStack)));
result.add(stack);
} catch (IOException e) {
Log.w(TAG, "Failed to resolve stack: " + e);
@@ -183,8 +183,7 @@ public class RecentsCreateFragment extends Fragment {
final TextView title = (TextView) convertView.findViewById(android.R.id.title);
final DocumentStack stack = getItem(position);
final RootInfo root = stack.getRoot(roots);
icon.setImageDrawable(root.loadIcon(context));
icon.setImageDrawable(stack.root.loadIcon(context));
final StringBuilder builder = new StringBuilder();
for (int i = stack.size() - 1; i >= 0; i--) {

View File

@@ -25,51 +25,64 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.text.format.DateUtils;
import android.util.Log;
public class RecentsProvider extends ContentProvider {
private static final String TAG = "RecentsProvider";
// TODO: offer view of recents that handles backend root resolution before
// returning cursor, include extra columns
public static final long MAX_HISTORY_IN_MILLIS = DateUtils.DAY_IN_MILLIS * 45;
public static final String AUTHORITY = "com.android.documentsui.recents";
private static final String AUTHORITY = "com.android.documentsui.recents";
private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int URI_RECENT_OPEN = 1;
private static final int URI_RECENT_CREATE = 2;
private static final int URI_RECENT = 1;
private static final int URI_STATE = 2;
private static final int URI_RESUME = 3;
static {
sMatcher.addURI(AUTHORITY, "recent_open", URI_RECENT_OPEN);
sMatcher.addURI(AUTHORITY, "recent_create", URI_RECENT_CREATE);
sMatcher.addURI(AUTHORITY, "recent", URI_RECENT);
// state/authority/rootId/docId
sMatcher.addURI(AUTHORITY, "state/*/*/*", URI_STATE);
// resume/packageName
sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME);
}
private static final String TABLE_RECENT_OPEN = "recent_open";
private static final String TABLE_RECENT_CREATE = "recent_create";
private static final String TABLE_RESUME = "resume";
public static final String TABLE_RECENT = "recent";
public static final String TABLE_STATE = "state";
public static final String TABLE_RESUME = "resume";
/**
* String of URIs pointing at a storage backend, stored as a JSON array,
* starting with root.
*/
public static final String COL_PATH = "path";
public static final String COL_URI = "uri";
public static final String COL_PACKAGE_NAME = "package_name";
public static final String COL_TIMESTAMP = "timestamp";
@Deprecated
public static Uri buildRecentOpen() {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY).appendPath("recent_open").build();
public static class RecentColumns {
public static final String STACK = "stack";
public static final String TIMESTAMP = "timestamp";
}
public static Uri buildRecentCreate() {
public static class StateColumns {
public static final String AUTHORITY = "authority";
public static final String ROOT_ID = Root.COLUMN_ROOT_ID;
public static final String DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID;
public static final String MODE = "mode";
public static final String SORT_ORDER = "sortOrder";
}
public static class ResumeColumns {
public static final String PACKAGE_NAME = "package_name";
public static final String STACK = "stack";
public static final String TIMESTAMP = "timestamp";
}
public static Uri buildRecent() {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY).appendPath("recent_create").build();
.authority(AUTHORITY).appendPath("recent").build();
}
public static Uri buildState(String authority, String rootId, String documentId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY)
.appendPath("state").appendPath(authority).appendPath(rootId).appendPath(documentId)
.build();
}
public static Uri buildResume(String packageName) {
@@ -83,35 +96,42 @@ public class RecentsProvider extends ContentProvider {
private static final String DB_NAME = "recents.db";
private static final int VERSION_INIT = 1;
private static final int VERSION_AS_BLOB = 3;
public DatabaseHelper(Context context) {
super(context, DB_NAME, null, VERSION_INIT);
super(context, DB_NAME, null, VERSION_AS_BLOB);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_RECENT_OPEN + " (" +
COL_URI + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
COL_TIMESTAMP + " INTEGER" +
db.execSQL("CREATE TABLE " + TABLE_RECENT + " (" +
RecentColumns.STACK + " BLOB PRIMARY KEY ON CONFLICT REPLACE," +
RecentColumns.TIMESTAMP + " INTEGER" +
")");
db.execSQL("CREATE TABLE " + TABLE_RECENT_CREATE + " (" +
COL_PATH + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
COL_TIMESTAMP + " INTEGER" +
db.execSQL("CREATE TABLE " + TABLE_STATE + " (" +
StateColumns.AUTHORITY + " TEXT," +
StateColumns.ROOT_ID + " TEXT," +
StateColumns.DOCUMENT_ID + " TEXT," +
StateColumns.MODE + " INTEGER," +
StateColumns.SORT_ORDER + " INTEGER," +
"PRIMARY KEY (" + StateColumns.AUTHORITY + ", " + StateColumns.ROOT_ID + ", "
+ StateColumns.DOCUMENT_ID + ")" +
")");
db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
COL_PACKAGE_NAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
COL_PATH + " TEXT," +
COL_TIMESTAMP + " INTEGER" +
ResumeColumns.PACKAGE_NAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
ResumeColumns.STACK + " BLOB," +
ResumeColumns.TIMESTAMP + " INTEGER" +
")");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database; wiping app data");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT_OPEN);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT_CREATE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_STATE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_RESUME);
onCreate(db);
}
@@ -128,22 +148,23 @@ public class RecentsProvider extends ContentProvider {
String sortOrder) {
final SQLiteDatabase db = mHelper.getReadableDatabase();
switch (sMatcher.match(uri)) {
case URI_RECENT_OPEN: {
return db.query(TABLE_RECENT_OPEN, projection,
buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
}
case URI_RECENT_CREATE: {
return db.query(TABLE_RECENT_CREATE, projection,
buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
}
case URI_RESUME: {
case URI_RECENT:
return db.query(TABLE_RECENT, projection,
RecentColumns.TIMESTAMP + "<" + MAX_HISTORY_IN_MILLIS, null, null, null,
null);
case URI_STATE:
final String authority = uri.getPathSegments().get(1);
final String rootId = uri.getPathSegments().get(2);
final String documentId = uri.getPathSegments().get(3);
return db.query(TABLE_STATE, projection, StateColumns.AUTHORITY + "=? AND "
+ StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?",
new String[] { authority, rootId, documentId }, null, null, null);
case URI_RESUME:
final String packageName = uri.getPathSegments().get(1);
return db.query(TABLE_RESUME, projection, COL_PACKAGE_NAME + "=?",
return db.query(TABLE_RESUME, projection, ResumeColumns.PACKAGE_NAME + "=?",
new String[] { packageName }, null, null, null);
}
default: {
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@@ -156,28 +177,37 @@ public class RecentsProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mHelper.getWritableDatabase();
switch (sMatcher.match(uri)) {
case URI_RECENT_OPEN: {
values.put(COL_TIMESTAMP, System.currentTimeMillis());
db.insert(TABLE_RECENT_OPEN, null, values);
db.delete(TABLE_RECENT_OPEN, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
case URI_RECENT:
values.put(RecentColumns.TIMESTAMP, System.currentTimeMillis());
db.insert(TABLE_RECENT, null, values);
db.delete(
TABLE_RECENT, RecentColumns.TIMESTAMP + ">" + MAX_HISTORY_IN_MILLIS, null);
return uri;
}
case URI_RECENT_CREATE: {
values.put(COL_TIMESTAMP, System.currentTimeMillis());
db.insert(TABLE_RECENT_CREATE, null, values);
db.delete(TABLE_RECENT_CREATE, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
case URI_STATE:
final String authority = uri.getPathSegments().get(1);
final String rootId = uri.getPathSegments().get(2);
final String documentId = uri.getPathSegments().get(3);
final ContentValues key = new ContentValues();
key.put(StateColumns.AUTHORITY, authority);
key.put(StateColumns.ROOT_ID, rootId);
key.put(StateColumns.DOCUMENT_ID, documentId);
// Ensure that row exists, then update with changed values
db.insertWithOnConflict(TABLE_STATE, null, key, SQLiteDatabase.CONFLICT_IGNORE);
db.update(TABLE_STATE, values, StateColumns.AUTHORITY + "=? AND "
+ StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?",
new String[] { authority, rootId, documentId });
return uri;
}
case URI_RESUME: {
case URI_RESUME:
final String packageName = uri.getPathSegments().get(1);
values.put(COL_PACKAGE_NAME, packageName);
values.put(COL_TIMESTAMP, System.currentTimeMillis());
values.put(ResumeColumns.PACKAGE_NAME, packageName);
values.put(ResumeColumns.TIMESTAMP, System.currentTimeMillis());
db.insert(TABLE_RESUME, null, values);
return uri;
}
default: {
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@@ -190,12 +220,4 @@ public class RecentsProvider extends ContentProvider {
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
private static String buildWhereOlder(long deltaMillis) {
return COL_TIMESTAMP + "<" + (System.currentTimeMillis() - deltaMillis);
}
private static String buildWhereYounger(long deltaMillis) {
return COL_TIMESTAMP + ">" + (System.currentTimeMillis() - deltaMillis);
}
}

View File

@@ -169,8 +169,9 @@ public class RootsCache {
if (state.localOnly && !localOnly) continue;
// Only include roots that serve requested content
final boolean overlap = MimePredicate.mimeMatches(root.mimeTypes, state.acceptMimes)
|| MimePredicate.mimeMatches(state.acceptMimes, root.mimeTypes);
final boolean overlap =
MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
if (!overlap) {
continue;
}

View File

@@ -20,6 +20,8 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
@@ -32,15 +34,16 @@ import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Comparator;
/**
* Representation of a {@link Document}.
*/
public class DocumentInfo implements Durable {
public class DocumentInfo implements Durable, Parcelable {
private static final int VERSION_INIT = 1;
private static final int VERSION_SPLIT_URI = 2;
public Uri uri;
public String authority;
public String documentId;
public String mimeType;
public String displayName;
public long lastModified;
@@ -49,13 +52,17 @@ public class DocumentInfo implements Durable {
public long size;
public int icon;
/** Derived fields that aren't persisted */
public Uri derivedUri;
public DocumentInfo() {
reset();
}
@Override
public void reset() {
uri = null;
authority = null;
documentId = null;
mimeType = null;
displayName = null;
lastModified = -1;
@@ -63,6 +70,8 @@ public class DocumentInfo implements Durable {
summary = null;
size = -1;
icon = 0;
derivedUri = null;
}
@Override
@@ -70,8 +79,10 @@ public class DocumentInfo implements Durable {
final int version = in.readInt();
switch (version) {
case VERSION_INIT:
final String rawUri = DurableUtils.readNullableString(in);
uri = rawUri != null ? Uri.parse(rawUri) : null;
throw new ProtocolException("Ignored upgrade");
case VERSION_SPLIT_URI:
authority = DurableUtils.readNullableString(in);
documentId = DurableUtils.readNullableString(in);
mimeType = DurableUtils.readNullableString(in);
displayName = DurableUtils.readNullableString(in);
lastModified = in.readLong();
@@ -79,6 +90,7 @@ public class DocumentInfo implements Durable {
summary = DurableUtils.readNullableString(in);
size = in.readLong();
icon = in.readInt();
deriveFields();
break;
default:
throw new ProtocolException("Unknown version " + version);
@@ -87,8 +99,9 @@ public class DocumentInfo implements Durable {
@Override
public void write(DataOutputStream out) throws IOException {
out.writeInt(VERSION_INIT);
DurableUtils.writeNullableString(out, uri.toString());
out.writeInt(VERSION_SPLIT_URI);
DurableUtils.writeNullableString(out, authority);
DurableUtils.writeNullableString(out, documentId);
DurableUtils.writeNullableString(out, mimeType);
DurableUtils.writeNullableString(out, displayName);
out.writeLong(lastModified);
@@ -98,11 +111,41 @@ public class DocumentInfo implements Durable {
out.writeInt(icon);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
DurableUtils.writeToParcel(dest, this);
}
public static final Creator<DocumentInfo> CREATOR = new Creator<DocumentInfo>() {
@Override
public DocumentInfo createFromParcel(Parcel in) {
final DocumentInfo doc = new DocumentInfo();
DurableUtils.readFromParcel(in, doc);
return doc;
}
@Override
public DocumentInfo[] newArray(int size) {
return new DocumentInfo[size];
}
};
public static DocumentInfo fromDirectoryCursor(Cursor cursor) {
final DocumentInfo doc = new DocumentInfo();
final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
doc.uri = DocumentsContract.buildDocumentUri(authority, docId);
return fromCursor(cursor, authority);
}
public static DocumentInfo fromCursor(Cursor cursor, String authority) {
final DocumentInfo doc = new DocumentInfo();
doc.authority = authority;
doc.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
doc.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
@@ -110,6 +153,7 @@ public class DocumentInfo implements Durable {
doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
doc.size = getCursorLong(cursor, Document.COLUMN_SIZE);
doc.icon = getCursorInt(cursor, Document.COLUMN_ICON);
doc.deriveFields();
return doc;
}
@@ -122,16 +166,7 @@ public class DocumentInfo implements Durable {
if (!cursor.moveToFirst()) {
throw new FileNotFoundException("Missing details for " + uri);
}
final DocumentInfo doc = new DocumentInfo();
doc.uri = uri;
doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
doc.size = getCursorLong(cursor, Document.COLUMN_SIZE);
doc.icon = getCursorInt(cursor, Document.COLUMN_ICON);
return doc;
return fromCursor(cursor, uri.getAuthority());
} catch (Throwable t) {
throw asFileNotFoundException(t);
} finally {
@@ -140,9 +175,13 @@ public class DocumentInfo implements Durable {
}
}
private void deriveFields() {
derivedUri = DocumentsContract.buildDocumentUri(authority, documentId);
}
@Override
public String toString() {
return "Document{name=" + displayName + ", uri=" + uri + "}";
return "Document{name=" + displayName + ", docId=" + documentId + "}";
}
public boolean isCreateSupported() {
@@ -189,42 +228,14 @@ public class DocumentInfo implements Durable {
}
}
/**
* Missing or null values are returned as 0.
*/
public static int getCursorInt(Cursor cursor, String columnName) {
final int index = cursor.getColumnIndex(columnName);
return (index != -1) ? cursor.getInt(index) : 0;
}
@Deprecated
public static class DisplayNameComparator implements Comparator<DocumentInfo> {
@Override
public int compare(DocumentInfo lhs, DocumentInfo rhs) {
final boolean leftDir = lhs.isDirectory();
final boolean rightDir = rhs.isDirectory();
if (leftDir != rightDir) {
return leftDir ? -1 : 1;
} else {
return compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
}
}
}
@Deprecated
public static class LastModifiedComparator implements Comparator<DocumentInfo> {
@Override
public int compare(DocumentInfo lhs, DocumentInfo rhs) {
return Long.compare(rhs.lastModified, lhs.lastModified);
}
}
@Deprecated
public static class SizeComparator implements Comparator<DocumentInfo> {
@Override
public int compare(DocumentInfo lhs, DocumentInfo rhs) {
return Long.compare(rhs.size, lhs.size);
}
}
public static FileNotFoundException asFileNotFoundException(Throwable t)
throws FileNotFoundException {
if (t instanceof FileNotFoundException) {

View File

@@ -16,8 +16,6 @@
package com.android.documentsui.model;
import com.android.documentsui.RootsCache;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -30,14 +28,13 @@ import java.util.LinkedList;
*/
public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
private static final int VERSION_INIT = 1;
private static final int VERSION_ADD_ROOT = 2;
public RootInfo getRoot(RootsCache roots) {
return roots.findRoot(getLast().uri);
}
public RootInfo root;
public String getTitle(RootsCache roots) {
if (size() == 1) {
return getRoot(roots).title;
public String getTitle() {
if (size() == 1 && root != null) {
return root.title;
} else if (size() > 1) {
return peek().displayName;
} else {
@@ -52,6 +49,7 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
@Override
public void reset() {
clear();
root = null;
}
@Override
@@ -59,6 +57,12 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
final int version = in.readInt();
switch (version) {
case VERSION_INIT:
throw new ProtocolException("Ignored upgrade");
case VERSION_ADD_ROOT:
if (in.readBoolean()) {
root = new RootInfo();
root.read(in);
}
final int size = in.readInt();
for (int i = 0; i < size; i++) {
final DocumentInfo doc = new DocumentInfo();
@@ -73,7 +77,13 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
@Override
public void write(DataOutputStream out) throws IOException {
out.writeInt(VERSION_INIT);
out.writeInt(VERSION_ADD_ROOT);
if (root != null) {
out.writeBoolean(true);
root.write(out);
} else {
out.writeBoolean(false);
}
final int size = size();
out.writeInt(size);
for (int i = 0; i < size; i++) {

View File

@@ -23,28 +23,121 @@ import static com.android.documentsui.model.DocumentInfo.getCursorString;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract.Root;
import com.android.documentsui.IconUtils;
import com.android.documentsui.R;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Objects;
/**
* Representation of a {@link Root}.
*/
public class RootInfo {
public class RootInfo implements Durable, Parcelable {
private static final int VERSION_INIT = 1;
public String authority;
public String rootId;
public int rootType;
public int flags;
public int icon;
public int localIcon;
public String title;
public String summary;
public String documentId;
public long availableBytes;
public String[] mimeTypes;
public String mimeTypes;
/** Derived fields that aren't persisted */
public String[] derivedMimeTypes;
public int derivedIcon;
public RootInfo() {
reset();
}
@Override
public void reset() {
authority = null;
rootId = null;
rootType = 0;
flags = 0;
icon = 0;
title = null;
summary = null;
documentId = null;
availableBytes = -1;
mimeTypes = null;
derivedMimeTypes = null;
derivedIcon = 0;
}
@Override
public void read(DataInputStream in) throws IOException {
final int version = in.readInt();
switch (version) {
case VERSION_INIT:
authority = DurableUtils.readNullableString(in);
rootId = DurableUtils.readNullableString(in);
rootType = in.readInt();
flags = in.readInt();
icon = in.readInt();
title = DurableUtils.readNullableString(in);
summary = DurableUtils.readNullableString(in);
documentId = DurableUtils.readNullableString(in);
availableBytes = in.readLong();
mimeTypes = DurableUtils.readNullableString(in);
deriveFields();
break;
default:
throw new ProtocolException("Unknown version " + version);
}
}
@Override
public void write(DataOutputStream out) throws IOException {
out.writeInt(VERSION_INIT);
DurableUtils.writeNullableString(out, authority);
DurableUtils.writeNullableString(out, rootId);
out.writeInt(rootType);
out.writeInt(flags);
out.writeInt(icon);
DurableUtils.writeNullableString(out, title);
DurableUtils.writeNullableString(out, summary);
DurableUtils.writeNullableString(out, documentId);
out.writeLong(availableBytes);
DurableUtils.writeNullableString(out, mimeTypes);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
DurableUtils.writeToParcel(dest, this);
}
public static final Creator<RootInfo> CREATOR = new Creator<RootInfo>() {
@Override
public RootInfo createFromParcel(Parcel in) {
final RootInfo root = new RootInfo();
DurableUtils.readFromParcel(in, root);
return root;
}
@Override
public RootInfo[] newArray(int size) {
return new RootInfo[size];
}
};
public static RootInfo fromRootsCursor(String authority, Cursor cursor) {
final RootInfo root = new RootInfo();
@@ -57,31 +150,38 @@ public class RootInfo {
root.summary = getCursorString(cursor, Root.COLUMN_SUMMARY);
root.documentId = getCursorString(cursor, Root.COLUMN_DOCUMENT_ID);
root.availableBytes = getCursorLong(cursor, Root.COLUMN_AVAILABLE_BYTES);
final String raw = getCursorString(cursor, Root.COLUMN_MIME_TYPES);
root.mimeTypes = (raw != null) ? raw.split("\n") : null;
// TODO: remove these special case icons
if ("com.android.externalstorage.documents".equals(authority)) {
root.localIcon = R.drawable.ic_root_sdcard;
}
if ("com.android.providers.downloads.documents".equals(authority)) {
root.localIcon = R.drawable.ic_root_download;
}
if ("com.android.providers.media.documents".equals(authority)) {
if ("image".equals(root.rootId)) {
root.localIcon = R.drawable.ic_doc_image;
} else if ("audio".equals(root.rootId)) {
root.localIcon = R.drawable.ic_doc_audio;
}
}
root.mimeTypes = getCursorString(cursor, Root.COLUMN_MIME_TYPES);
root.deriveFields();
return root;
}
private void deriveFields() {
derivedMimeTypes = (mimeTypes != null) ? mimeTypes.split("\n") : null;
// TODO: remove these special case icons
if ("com.android.externalstorage.documents".equals(authority)) {
derivedIcon = R.drawable.ic_root_sdcard;
}
if ("com.android.providers.downloads.documents".equals(authority)) {
derivedIcon = R.drawable.ic_root_download;
}
if ("com.android.providers.media.documents".equals(authority)) {
if ("image".equals(rootId)) {
derivedIcon = R.drawable.ic_doc_image;
} else if ("audio".equals(rootId)) {
derivedIcon = R.drawable.ic_doc_audio;
}
}
}
@Override
public String toString() {
return "Root{title=" + title + ", rootId=" + rootId + "}";
}
public Drawable loadIcon(Context context) {
if (localIcon != 0) {
return context.getResources().getDrawable(localIcon);
if (derivedIcon != 0) {
return context.getResources().getDrawable(derivedIcon);
} else {
return IconUtils.loadPackageIcon(context, authority, icon);
}