summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-08-05 17:56:48 -0700
committerJeff Sharkey <jsharkey@android.com>2013-08-05 20:14:12 -0700
commitef7184a1aa0be5d496a5cb495a0f9e11f342af44 (patch)
treedb95537171aedc73dd595ee6d09cf5433cb8697b /packages
parentdc2963aecaf38bf53d6de82957412a486049c207 (diff)
downloadframeworks_base-ef7184a1aa0be5d496a5cb495a0f9e11f342af44.zip
frameworks_base-ef7184a1aa0be5d496a5cb495a0f9e11f342af44.tar.gz
frameworks_base-ef7184a1aa0be5d496a5cb495a0f9e11f342af44.tar.bz2
More recents work; filtering and sorting.
Update DirectoryFragment to render List<Document>, making it more general purpose. Feed it documents either from a backend Cursor or after resolving fields from a recents Cursor. Start in recents when no persisted stack available. Synthesize a root for recents. Local directory filtering and sorting using predicates and comparators, all performed on background thread. Introduce UriDerivativeLoader which handles ContentObserver updates while producing a derivative work of a Cursor. Split data model classes into separate files. Change-Id: Idb88b4ee22c58c8e508328e678877f7e4c978533
Diffstat (limited to 'packages')
-rw-r--r--packages/DocumentsUI/res/layout/activity.xml2
-rw-r--r--packages/DocumentsUI/res/values/strings.xml2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java170
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java90
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java338
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java64
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java144
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/Document.java169
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java121
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/Root.java92
10 files changed, 855 insertions, 337 deletions
diff --git a/packages/DocumentsUI/res/layout/activity.xml b/packages/DocumentsUI/res/layout/activity.xml
index eb6d803..d4a01d3 100644
--- a/packages/DocumentsUI/res/layout/activity.xml
+++ b/packages/DocumentsUI/res/layout/activity.xml
@@ -39,7 +39,7 @@
<ListView
android:id="@+id/roots_list"
- android:layout_width="300dp"
+ android:layout_width="250dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#fff" />
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 665f3b1..3eda207 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -39,4 +39,6 @@
<string name="save_error">Failed to save document</string>
+ <string name="root_recent">Recent</string>
+
</resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 2740e53..f6f3f9c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -21,13 +21,10 @@ import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
-import android.content.CursorLoader;
import android.content.Loader;
-import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.DocumentColumns;
import android.text.format.DateUtils;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
@@ -41,17 +38,20 @@ import android.widget.AbsListView;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
-import android.widget.CursorAdapter;
+import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.android.documentsui.DocumentsActivity.DisplayState;
-import com.android.documentsui.DocumentsActivity.Document;
+import com.android.documentsui.model.Document;
+import com.android.internal.util.Predicate;
import com.google.android.collect.Lists;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
/**
* Display the documents inside a single directory.
@@ -59,23 +59,21 @@ import java.util.ArrayList;
public class DirectoryFragment extends Fragment {
// TODO: show storage backend in item views when requested
- // TODO: apply sort order locally
- // TODO: apply MIME filtering locally
private ListView mListView;
private GridView mGridView;
private AbsListView mCurrentView;
- private static final int TYPE_NORMAL = 1;
- private static final int TYPE_SEARCH = 2;
- private static final int TYPE_RECENT_OPEN = 3;
- private static final int TYPE_RECENT_CREATE = 4;
+ public static final int TYPE_NORMAL = 1;
+ public static final int TYPE_SEARCH = 2;
+ public static final int TYPE_RECENT_OPEN = 3;
+ public static final int TYPE_RECENT_CREATE = 4;
private int mType = TYPE_NORMAL;
private DocumentsAdapter mAdapter;
- private LoaderCallbacks<Cursor> mCallbacks;
+ private LoaderCallbacks<List<Document>> mCallbacks;
private static final String EXTRA_URI = "uri";
@@ -93,6 +91,11 @@ public class DirectoryFragment extends Fragment {
ft.commitAllowingStateLoss();
}
+ public static DirectoryFragment get(FragmentManager fm) {
+ // TODO: deal with multiple directories shown at once
+ return (DirectoryFragment) fm.findFragmentById(R.id.directory);
+ }
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -114,7 +117,7 @@ public class DirectoryFragment extends Fragment {
mGridView.setOnItemClickListener(mItemListener);
mGridView.setMultiChoiceModeListener(mMultiListener);
- mAdapter = new DocumentsAdapter(context);
+ mAdapter = new DocumentsAdapter();
updateMode();
final Uri uri = getArguments().getParcelable(EXTRA_URI);
@@ -129,18 +132,10 @@ public class DirectoryFragment extends Fragment {
mType = TYPE_NORMAL;
}
- mCallbacks = new LoaderCallbacks<Cursor>() {
+ mCallbacks = new LoaderCallbacks<List<Document>>() {
@Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ public Loader<List<Document>> onCreateLoader(int id, Bundle args) {
final DisplayState state = getDisplayState(DirectoryFragment.this);
- final String sortOrder;
- if (state.sortBy == DisplayState.SORT_BY_NAME) {
- sortOrder = DocumentColumns.DISPLAY_NAME + " ASC";
- } else if (state.sortBy == DisplayState.SORT_BY_DATE) {
- sortOrder = DocumentColumns.LAST_MODIFIED + " DESC";
- } else {
- sortOrder = null;
- }
final Uri contentsUri;
if (mType == TYPE_NORMAL) {
@@ -149,17 +144,29 @@ public class DirectoryFragment extends Fragment {
contentsUri = uri;
}
- return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
+ final Predicate<Document> filter = new MimePredicate(state.acceptMimes);
+
+ final Comparator<Document> sortOrder;
+ if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN
+ || mType == TYPE_RECENT_CREATE) {
+ sortOrder = new Document.DateComparator();
+ } else if (state.sortOrder == DisplayState.SORT_ORDER_NAME) {
+ sortOrder = new Document.NameComparator();
+ } else {
+ throw new IllegalArgumentException("Unknown sort order " + state.sortOrder);
+ }
+
+ return new DirectoryLoader(context, contentsUri, mType, filter, sortOrder);
}
@Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- mAdapter.swapCursor(data);
+ public void onLoadFinished(Loader<List<Document>> loader, List<Document> data) {
+ mAdapter.swapDocuments(data);
}
@Override
- public void onLoaderReset(Loader<Cursor> loader) {
- mAdapter.swapCursor(null);
+ public void onLoaderReset(Loader<List<Document>> loader) {
+ mAdapter.swapDocuments(null);
}
};
@@ -243,16 +250,16 @@ public class DirectoryFragment extends Fragment {
}
}
- public void updateSortBy() {
+ public void updateSortOrder() {
getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
+ mListView.smoothScrollToPosition(0);
+ mGridView.smoothScrollToPosition(0);
}
private OnItemClickListener mItemListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final Cursor cursor = (Cursor) mAdapter.getItem(position);
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
- final Document doc = Document.fromCursor(uri, cursor);
+ final Document doc = mAdapter.getItem(position);
((DocumentsActivity) getActivity()).onDocumentPicked(doc);
}
};
@@ -279,8 +286,8 @@ public class DirectoryFragment extends Fragment {
final int size = checked.size();
for (int i = 0; i < size; i++) {
if (checked.valueAt(i)) {
- final Cursor cursor = (Cursor) mAdapter.getItem(checked.keyAt(i));
- docs.add(Document.fromCursor(uri, cursor));
+ final Document doc = mAdapter.getItem(checked.keyAt(i));
+ docs.add(doc);
}
}
@@ -300,11 +307,9 @@ public class DirectoryFragment extends Fragment {
public void onItemCheckedStateChanged(
ActionMode mode, int position, long id, boolean checked) {
if (checked) {
- final Cursor cursor = (Cursor) mAdapter.getItem(position);
- final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
-
// Directories cannot be checked
- if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
+ final Document doc = mAdapter.getItem(position);
+ if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
mCurrentView.setItemChecked(position, false);
}
}
@@ -318,61 +323,68 @@ public class DirectoryFragment extends Fragment {
return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
}
- private class DocumentsAdapter extends CursorAdapter {
- public DocumentsAdapter(Context context) {
- super(context, null, false);
+ private class DocumentsAdapter extends BaseAdapter {
+ private List<Document> mDocuments;
+
+ public DocumentsAdapter() {
}
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- final LayoutInflater inflater = LayoutInflater.from(context);
- final DisplayState state = getDisplayState(DirectoryFragment.this);
- if (state.mode == DisplayState.MODE_LIST) {
- return inflater.inflate(R.layout.item_doc_list, parent, false);
- } else if (state.mode == DisplayState.MODE_GRID) {
- return inflater.inflate(R.layout.item_doc_grid, parent, false);
- } else {
- throw new IllegalStateException();
- }
+ public void swapDocuments(List<Document> documents) {
+ mDocuments = documents;
+ notifyDataSetChanged();
}
@Override
- public void bindView(View view, Context context, Cursor cursor) {
- final TextView title = (TextView) view.findViewById(android.R.id.title);
- final TextView summary = (TextView) view.findViewById(android.R.id.summary);
- final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
-
- final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
- final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
- final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
- final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
- final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
-
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
- if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
- final Uri childUri = DocumentsContract.buildDocumentUri(uri, docId);
- icon.setImageURI(childUri);
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final Context context = parent.getContext();
+
+ if (convertView == null) {
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ final DisplayState state = getDisplayState(DirectoryFragment.this);
+ if (state.mode == DisplayState.MODE_LIST) {
+ convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
+ } else if (state.mode == DisplayState.MODE_GRID) {
+ convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
+ } else {
+ throw new IllegalStateException();
+ }
+ }
+
+ final Document doc = getItem(position);
+
+ final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+ final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
+ final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+
+ if (doc.isThumbnailSupported()) {
+ // TODO: load thumbnails async
+ icon.setImageURI(doc.uri);
} else {
icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
- context, uri.getAuthority(), mimeType));
+ context, doc.uri.getAuthority(), doc.mimeType));
}
- title.setText(displayName);
+ title.setText(doc.displayName);
if (summary != null) {
- summary.setText(DateUtils.getRelativeTimeSpanString(lastModified));
+ summary.setText(DateUtils.getRelativeTimeSpanString(doc.lastModified));
}
+
+ return convertView;
}
- }
- public static String getCursorString(Cursor cursor, String columnName) {
- return cursor.getString(cursor.getColumnIndex(columnName));
- }
+ @Override
+ public int getCount() {
+ return mDocuments != null ? mDocuments.size() : 0;
+ }
- public static long getCursorLong(Cursor cursor, String columnName) {
- return cursor.getLong(cursor.getColumnIndex(columnName));
- }
+ @Override
+ public Document getItem(int position) {
+ return mDocuments.get(position);
+ }
- public static int getCursorInt(Cursor cursor, String columnName) {
- return cursor.getInt(cursor.getColumnIndex(columnName));
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).uri.hashCode();
+ }
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
new file mode 100644
index 0000000..a50c312
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -0,0 +1,90 @@
+/*
+ * 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.DirectoryFragment.TYPE_NORMAL;
+import static com.android.documentsui.DirectoryFragment.TYPE_RECENT_OPEN;
+import static com.android.documentsui.DirectoryFragment.TYPE_SEARCH;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.CancellationSignal;
+
+import com.android.documentsui.model.Document;
+import com.android.internal.util.Predicate;
+import com.google.android.collect.Lists;
+
+import libcore.io.IoUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class DirectoryLoader extends UriDerivativeLoader<List<Document>> {
+
+ private final int mType;
+ private Predicate<Document> mFilter;
+ private Comparator<Document> mSortOrder;
+
+ public DirectoryLoader(Context context, Uri uri, int type, Predicate<Document> filter,
+ Comparator<Document> sortOrder) {
+ super(context, uri);
+ mType = type;
+ mFilter = filter;
+ mSortOrder = sortOrder;
+ }
+
+ @Override
+ public List<Document> loadInBackground(Uri uri, CancellationSignal signal) {
+ final ArrayList<Document> result = Lists.newArrayList();
+
+ // TODO: send selection and sorting hints to backend
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
+ try {
+ while (cursor != null && cursor.moveToNext()) {
+ final Document doc;
+ switch (mType) {
+ case TYPE_NORMAL:
+ case TYPE_SEARCH:
+ doc = Document.fromDirectoryCursor(uri, cursor);
+ break;
+ case TYPE_RECENT_OPEN:
+ doc = Document.fromRecentOpenCursor(resolver, cursor);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown type");
+ }
+
+ if (mFilter == null || mFilter.apply(doc)) {
+ result.add(doc);
+ }
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ }
+
+ if (mSortOrder != null) {
+ Collections.sort(result, mSortOrder);
+ }
+
+ return result;
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 8f2e61d..0cbd1cb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -16,10 +16,6 @@
package com.android.documentsui;
-import static com.android.documentsui.DirectoryFragment.getCursorInt;
-import static com.android.documentsui.DirectoryFragment.getCursorLong;
-import static com.android.documentsui.DirectoryFragment.getCursorString;
-
import android.app.ActionBar;
import android.app.ActionBar.OnNavigationListener;
import android.app.Activity;
@@ -28,6 +24,7 @@ import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.content.ClipData;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -38,10 +35,6 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -49,14 +42,12 @@ import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
-import android.provider.DocumentsContract.RootColumns;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.DrawerLayout.DrawerListener;
-import android.util.AttributeSet;
import android.util.Log;
-import android.util.Xml;
+import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -74,17 +65,16 @@ import android.widget.SearchView.OnQueryTextListener;
import android.widget.TextView;
import android.widget.Toast;
+import com.android.documentsui.model.Document;
+import com.android.documentsui.model.DocumentsProviderInfo;
+import com.android.documentsui.model.DocumentsProviderInfo.Icon;
+import com.android.documentsui.model.Root;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
-import libcore.io.IoUtils;
-
import org.json.JSONArray;
import org.json.JSONException;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -92,7 +82,7 @@ import java.util.LinkedList;
import java.util.List;
public class DocumentsActivity extends Activity {
- private static final String TAG = "Documents";
+ public static final String TAG = "Documents";
// TODO: share backend root cache with recents provider
@@ -102,19 +92,24 @@ public class DocumentsActivity extends Activity {
private static final int ACTION_CREATE = 2;
private int mAction;
- private String[] mAcceptMimes;
private SearchView mSearchView;
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
+ private Root mCurrentRoot;
+
+ /** Map from authority to cached info */
private static HashMap<String, DocumentsProviderInfo> sProviders = Maps.newHashMap();
- private static HashMap<String, Root> sRoots = Maps.newHashMap();
+ /** Map from (authority+rootId) to cached info */
+ private static HashMap<Pair<String, String>, Root> sRoots = Maps.newHashMap();
// TODO: remove once adapter split by type
private static ArrayList<Root> sRootsList = Lists.newArrayList();
+ private static Root sRecentOpenRoot;
+
private RootsAdapter mRootsAdapter;
private ListView mRootsList;
@@ -130,19 +125,20 @@ public class DocumentsActivity extends Activity {
final String action = intent.getAction();
if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
mAction = ACTION_OPEN;
- mDisplayState.allowMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
+ mDisplayState.allowMultiple = intent.getBooleanExtra(
+ Intent.EXTRA_ALLOW_MULTIPLE, false);
} else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
mAction = ACTION_CREATE;
mDisplayState.allowMultiple = false;
}
if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
- mAcceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
+ mDisplayState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
} else {
- mAcceptMimes = new String[] { intent.getType() };
+ mDisplayState.acceptMimes = new String[] { intent.getType() };
}
- if (mimeMatches("image/*", mAcceptMimes)) {
+ if (MimePredicate.mimeMatches("image/*", mDisplayState.acceptMimes)) {
mDisplayState.mode = DisplayState.MODE_GRID;
} else {
mDisplayState.mode = DisplayState.MODE_LIST;
@@ -169,6 +165,8 @@ public class DocumentsActivity extends Activity {
mDrawerLayout.setDrawerListener(mDrawerListener);
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+ mDrawerLayout.openDrawer(mRootsList);
+
updateRoots();
// Restore last stack for calling package
@@ -186,6 +184,11 @@ public class DocumentsActivity extends Activity {
cursor.close();
}
+ // Start in recents if no restored stack
+ if (mStack.isEmpty()) {
+ onRootPicked(sRecentOpenRoot);
+ }
+
updateDirectoryFragment();
}
@@ -236,11 +239,18 @@ public class DocumentsActivity extends Activity {
}
} else {
- actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
final Root root = getCurrentRoot();
actionBar.setIcon(root != null ? root.icon : null);
- actionBar.setTitle(null);
- actionBar.setListNavigationCallbacks(mSortAdapter, mSortListener);
+
+ if (getCurrentRoot().isRecents) {
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ actionBar.setTitle(root.title);
+ } else {
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ actionBar.setTitle(null);
+ actionBar.setListNavigationCallbacks(mSortAdapter, mSortListener);
+ actionBar.setSelectedNavigationItem(mDisplayState.sortOrder);
+ }
if (mStack.size() > 1) {
mDrawerToggle.setDrawerIndicatorEnabled(false);
@@ -395,18 +405,14 @@ public class DocumentsActivity extends Activity {
private OnNavigationListener mSortListener = new OnNavigationListener() {
@Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
- // TODO: request updated sort order
+ mDisplayState.sortOrder = itemPosition;
+ DirectoryFragment.get(getFragmentManager()).updateSortOrder();
return true;
}
};
public Root getCurrentRoot() {
- final Document cwd = getCurrentDirectory();
- if (cwd != null) {
- return sRoots.get(DocumentsContract.getRootId(cwd.uri));
- } else {
- return null;
- }
+ return mCurrentRoot;
}
public Document getCurrentDirectory() {
@@ -422,15 +428,19 @@ public class DocumentsActivity extends Activity {
final Document cwd = getCurrentDirectory();
if (cwd != null) {
DirectoryFragment.show(fm, cwd.uri);
- mDrawerLayout.closeDrawer(mRootsList);
- } else {
- mDrawerLayout.openDrawer(mRootsList);
}
updateActionBar();
invalidateOptionsMenu();
dumpStack();
}
+ public void onRootPicked(Root root) {
+ // Clear entire backstack and start in new root
+ mStack.clear();
+ mCurrentRoot = root;
+ onDocumentPicked(Document.fromRoot(getContentResolver(), root));
+ }
+
public void onDocumentPicked(Document doc) {
final FragmentManager fm = getFragmentManager();
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
@@ -471,6 +481,8 @@ public class DocumentsActivity extends Activity {
}
private String saveStack() {
+ if (mCurrentRoot.isRecents) return null;
+
final JSONArray stack = new JSONArray();
for (int i = 0; i < mStack.size(); i++) {
stack.put(mStack.get(i).uri);
@@ -481,6 +493,8 @@ public class DocumentsActivity extends Activity {
private void restoreStack(String rawStack) {
Log.d(TAG, "restoreStack: " + rawStack);
mStack.clear();
+
+ if (rawStack == null) return;
try {
final JSONArray stack = new JSONArray(rawStack);
for (int i = 0; i < stack.length(); i++) {
@@ -491,6 +505,14 @@ public class DocumentsActivity extends Activity {
} catch (JSONException e) {
Log.w(TAG, "Failed to decode stack", e);
}
+
+ // TODO: handle roots that have gone missing
+ final Document cwd = getCurrentDirectory();
+ if (cwd != null) {
+ final String authority = cwd.uri.getAuthority();
+ final String rootId = DocumentsContract.getRootId(cwd.uri);
+ mCurrentRoot = sRoots.get(Pair.create(authority, rootId));
+ }
}
private void onFinished(Uri... uris) {
@@ -525,7 +547,8 @@ public class DocumentsActivity extends Activity {
if (uris.length == 1) {
intent.setData(uris[0]);
} else if (uris.length > 1) {
- final ClipData clipData = new ClipData(null, mAcceptMimes, new ClipData.Item(uris[0]));
+ final ClipData clipData = new ClipData(
+ null, mDisplayState.acceptMimes, new ClipData.Item(uris[0]));
for (int i = 1; i < uris.length; i++) {
clipData.addItem(new ClipData.Item(uris[i]));
}
@@ -542,162 +565,16 @@ public class DocumentsActivity extends Activity {
}
public static class DisplayState {
- public int mode;
- public int sortBy;
- public boolean allowMultiple;
+ public int mode = MODE_LIST;
+ public String[] acceptMimes;
+ public int sortOrder = SORT_ORDER_NAME;
+ public boolean allowMultiple = false;
public static final int MODE_LIST = 0;
public static final int MODE_GRID = 1;
- public static final int SORT_BY_NAME = 0;
- public static final int SORT_BY_DATE = 1;
- }
-
- public static class Root {
- public DocumentsProviderInfo info;
- public String rootId;
- public int rootType;
- public Uri uri;
- public Drawable icon;
- public String title;
- public String summary;
-
- public static Root fromCursor(
- Context context, DocumentsProviderInfo info, Cursor cursor) {
- final PackageManager pm = context.getPackageManager();
-
- final Root root = new Root();
- root.info = info;
- root.rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
- root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
- root.uri = DocumentsContract.buildDocumentUri(
- info.providerInfo.authority, root.rootId, DocumentsContract.ROOT_DOC_ID);
- root.icon = info.providerInfo.loadIcon(pm);
- root.title = info.providerInfo.loadLabel(pm).toString();
- root.summary = null;
-
- final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
- if (icon != 0) {
- try {
- root.icon = pm.getResourcesForApplication(info.providerInfo.applicationInfo)
- .getDrawable(icon);
- } catch (NotFoundException e) {
- throw new RuntimeException(e);
- } catch (NameNotFoundException e) {
- throw new RuntimeException(e);
- }
- }
-
- final String title = cursor.getString(cursor.getColumnIndex(RootColumns.TITLE));
- if (title != null) {
- root.title = title;
- }
-
- root.summary = cursor.getString(cursor.getColumnIndex(RootColumns.SUMMARY));
-
- return root;
- }
- }
-
- public static class DocumentsProviderInfo {
- public ProviderInfo providerInfo;
- public boolean customRoots;
- public List<Icon> customIcons;
- }
-
- public static class Icon {
- public String mimeType;
- public Drawable icon;
- }
-
- public static class Document {
- public Uri uri;
- public String mimeType;
- public String displayName;
- public long lastModified;
- public int flags;
-
- public static Document fromCursor(Uri parent, Cursor cursor) {
- final String authority = parent.getAuthority();
- final String rootId = DocumentsContract.getRootId(parent);
- final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
-
- final Document doc = new Document();
- doc.uri = DocumentsContract.buildDocumentUri(authority, rootId, docId);
- doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
- doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
- doc.lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
- doc.flags = getCursorInt(cursor, DocumentColumns.FLAGS);
- return doc;
- }
-
- public static Document fromUri(ContentResolver resolver, Uri uri) {
- final Document doc = new Document();
- doc.uri = uri;
-
- final Cursor cursor = resolver.query(uri, null, null, null, null);
- try {
- if (!cursor.moveToFirst()) {
- throw new IllegalArgumentException("Missing details for " + uri);
- }
- doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
- doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
- doc.lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
- doc.flags = getCursorInt(cursor, DocumentColumns.FLAGS);
- } finally {
- cursor.close();
- }
-
- return doc;
- }
-
- public static Document fromSearch(Uri relatedUri, String query) {
- final Document doc = new Document();
- doc.uri = DocumentsContract.buildSearchUri(relatedUri, query);
- doc.mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
- doc.displayName = query;
- doc.lastModified = System.currentTimeMillis();
- doc.flags = 0;
- return doc;
- }
-
- @Override
- public String toString() {
- return "'" + displayName + "' " + uri;
- }
-
- public boolean isCreateSupported() {
- return (flags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
- }
-
- public boolean isSearchSupported() {
- return (flags & DocumentsContract.FLAG_SUPPORTS_SEARCH) != 0;
- }
-
- public boolean isThumbnailSupported() {
- return (flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0;
- }
- }
-
- public static boolean mimeMatches(String filter, String[] tests) {
- for (String test : tests) {
- if (mimeMatches(filter, test)) {
- return true;
- }
- }
- return false;
- }
-
- public static boolean mimeMatches(String filter, String test) {
- if (filter.equals(test)) {
- return true;
- } else if ("*/*".equals(filter)) {
- return true;
- } else if (filter.endsWith("/*")) {
- return filter.regionMatches(0, test, 0, filter.indexOf('/'));
- } else {
- return false;
- }
+ public static final int SORT_ORDER_NAME = 0;
+ public static final int SORT_ORDER_DATE = 1;
}
public static Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
@@ -705,7 +582,7 @@ public class DocumentsActivity extends Activity {
final DocumentsProviderInfo info = sProviders.get(authority);
if (info != null) {
for (Icon icon : info.customIcons) {
- if (mimeMatches(icon.mimeType, mimeType)) {
+ if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
return icon.icon;
}
}
@@ -728,9 +605,6 @@ public class DocumentsActivity extends Activity {
}
}
- private static final String TAG_DOCUMENTS_PROVIDER = "documents-provider";
- private static final String TAG_ICON = "icon";
-
/**
* Gather roots from all known storage providers.
*/
@@ -739,13 +613,24 @@ public class DocumentsActivity extends Activity {
sRoots.clear();
sRootsList.clear();
+ final Context context = this;
final PackageManager pm = getPackageManager();
+
+ // Create special roots, like recents
+ {
+ final Root root = Root.buildRecentOpen(context);
+ sRootsList.add(root);
+ sRecentOpenRoot = root;
+ }
+
+ // Query for other storage backends
final List<ProviderInfo> providers = pm.queryContentProviders(
null, -1, PackageManager.GET_META_DATA);
for (ProviderInfo providerInfo : providers) {
if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
- final DocumentsProviderInfo info = parseInfo(this, providerInfo);
+ final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
+ this, providerInfo);
if (info == null) {
Log.w(TAG, "Missing info for " + providerInfo);
continue;
@@ -760,7 +645,7 @@ public class DocumentsActivity extends Activity {
try {
while (cursor.moveToNext()) {
final Root root = Root.fromCursor(this, info, cursor);
- sRoots.put(root.rootId, root);
+ sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
sRootsList.add(root);
}
} finally {
@@ -770,72 +655,11 @@ public class DocumentsActivity extends Activity {
}
}
- private static DocumentsProviderInfo parseInfo(Context context, ProviderInfo providerInfo) {
- final DocumentsProviderInfo info = new DocumentsProviderInfo();
- info.providerInfo = providerInfo;
- info.customIcons = Lists.newArrayList();
-
- final PackageManager pm = context.getPackageManager();
- final Resources res;
- try {
- res = pm.getResourcesForApplication(providerInfo.applicationInfo);
- } catch (NameNotFoundException e) {
- Log.w(TAG, "Failed to find resources for " + providerInfo, e);
- return null;
- }
-
- XmlResourceParser parser = null;
- try {
- parser = providerInfo.loadXmlMetaData(
- pm, DocumentsContract.META_DATA_DOCUMENT_PROVIDER);
- AttributeSet attrs = Xml.asAttributeSet(parser);
-
- int type = 0;
- while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
- final String tag = parser.getName();
- if (type == XmlPullParser.START_TAG && TAG_DOCUMENTS_PROVIDER.equals(tag)) {
- final TypedArray a = res.obtainAttributes(
- attrs, com.android.internal.R.styleable.DocumentsProviderInfo);
- info.customRoots = a.getBoolean(
- com.android.internal.R.styleable.DocumentsProviderInfo_customRoots,
- false);
- a.recycle();
-
- } else if (type == XmlPullParser.START_TAG && TAG_ICON.equals(tag)) {
- final TypedArray a = res.obtainAttributes(
- attrs, com.android.internal.R.styleable.Icon);
- final Icon icon = new Icon();
- icon.mimeType = a.getString(com.android.internal.R.styleable.Icon_mimeType);
- icon.icon = a.getDrawable(com.android.internal.R.styleable.Icon_icon);
- info.customIcons.add(icon);
- a.recycle();
- }
- }
- } catch (IOException e){
- Log.w(TAG, "Failed to parse metadata", e);
- return null;
- } catch (XmlPullParserException e) {
- Log.w(TAG, "Failed to parse metadata", e);
- return null;
- } finally {
- IoUtils.closeQuietly(parser);
- }
-
- return info;
- }
-
private OnItemClickListener mRootsListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- // Clear entire backstack and start in new root
- mStack.clear();
-
final Root root = mRootsAdapter.getItem(position);
-
- final ContentResolver resolver = getContentResolver();
- final Document doc = Document.fromUri(resolver, root.uri);
- onDocumentPicked(doc);
-
+ onRootPicked(root);
mDrawerLayout.closeDrawers();
}
};
diff --git a/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java b/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java
new file mode 100644
index 0000000..0d2f381
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider.DocumentsContract;
+
+import com.android.documentsui.model.Document;
+import com.android.internal.util.Predicate;
+
+public class MimePredicate implements Predicate<Document> {
+ private final String[] mFilters;
+
+ public MimePredicate(String[] filters) {
+ mFilters = filters;
+ }
+
+ @Override
+ public boolean apply(Document doc) {
+ if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
+ return true;
+ }
+ for (String filter : mFilters) {
+ if (mimeMatches(filter, doc.mimeType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean mimeMatches(String filter, String[] tests) {
+ for (String test : tests) {
+ if (mimeMatches(filter, test)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean mimeMatches(String filter, String test) {
+ if (filter.equals(test)) {
+ return true;
+ } else if ("*/*".equals(filter)) {
+ return true;
+ } else if (filter.endsWith("/*")) {
+ return filter.regionMatches(0, test, 0, filter.indexOf('/'));
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java b/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java
new file mode 100644
index 0000000..1b88af4
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java
@@ -0,0 +1,144 @@
+/*
+ * 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.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+
+/**
+ * Loader that derives its data from a Uri. Watches for {@link ContentObserver}
+ * changes while started, manages {@link CancellationSignal}, and caches
+ * returned results.
+ */
+public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
+ private final ForceLoadContentObserver mObserver;
+ private boolean mObserving;
+
+ private final Uri mUri;
+
+ private T mResult;
+ private CancellationSignal mCancellationSignal;
+
+ @Override
+ public final T loadInBackground() {
+ synchronized (this) {
+ if (isLoadInBackgroundCanceled()) {
+ throw new OperationCanceledException();
+ }
+ mCancellationSignal = new CancellationSignal();
+ }
+ try {
+ return loadInBackground(mUri, mCancellationSignal);
+ } finally {
+ synchronized (this) {
+ mCancellationSignal = null;
+ }
+ }
+ }
+
+ public abstract T loadInBackground(Uri uri, CancellationSignal signal);
+
+ @Override
+ public void cancelLoadInBackground() {
+ super.cancelLoadInBackground();
+
+ synchronized (this) {
+ if (mCancellationSignal != null) {
+ mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ @Override
+ public void deliverResult(T result) {
+ if (isReset()) {
+ closeQuietly(result);
+ return;
+ }
+ T oldResult = mResult;
+ mResult = result;
+
+ if (isStarted()) {
+ super.deliverResult(result);
+ }
+
+ if (oldResult != null && oldResult != result) {
+ closeQuietly(oldResult);
+ }
+ }
+
+ public UriDerivativeLoader(Context context, Uri uri) {
+ super(context);
+ mObserver = new ForceLoadContentObserver();
+ mUri = uri;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (!mObserving) {
+ getContext().getContentResolver().registerContentObserver(mUri, false, mObserver);
+ mObserving = true;
+ }
+ if (mResult != null) {
+ deliverResult(mResult);
+ }
+ if (takeContentChanged() || mResult == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ public void onCanceled(T result) {
+ closeQuietly(result);
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+
+ closeQuietly(mResult);
+ mResult = null;
+
+ if (mObserving) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserving = false;
+ }
+ }
+
+ private void closeQuietly(T result) {
+ if (result instanceof AutoCloseable) {
+ try {
+ ((AutoCloseable) result).close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
new file mode 100644
index 0000000..94b9093
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
@@ -0,0 +1,169 @@
+/*
+ * 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.model;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.DocumentColumns;
+
+import com.android.documentsui.RecentsProvider;
+
+import java.util.Comparator;
+
+/**
+ * Representation of a single document.
+ */
+public class Document {
+ public final Uri uri;
+ public final String mimeType;
+ public final String displayName;
+ public final long lastModified;
+ public final int flags;
+
+ private Document(Uri uri, String mimeType, String displayName, long lastModified, int flags) {
+ this.uri = uri;
+ this.mimeType = mimeType;
+ this.displayName = displayName;
+ this.lastModified = lastModified;
+ this.flags = flags;
+ }
+
+ public static Document fromRoot(ContentResolver resolver, Root root) {
+ if (root.isRecents) {
+ final Uri uri = root.uri;
+ final String mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
+ final String displayName = root.title;
+ final long lastModified = -1;
+ final int flags = 0;
+ return new Document(uri, mimeType, displayName, lastModified, flags);
+ } else {
+ return fromUri(resolver, root.uri);
+ }
+ }
+
+ public static Document fromDirectoryCursor(Uri parent, Cursor cursor) {
+ final String authority = parent.getAuthority();
+ final String rootId = DocumentsContract.getRootId(parent);
+ final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
+
+ final Uri uri = DocumentsContract.buildDocumentUri(authority, rootId, docId);
+ final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
+ final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
+ final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
+ final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
+
+ return new Document(uri, mimeType, displayName, lastModified, flags);
+ }
+
+ public static Document fromRecentOpenCursor(ContentResolver resolver, Cursor cursor) {
+ final Uri uri = Uri.parse(getCursorString(cursor, RecentsProvider.COL_URI));
+ final long lastModified = getCursorLong(cursor, RecentsProvider.COL_TIMESTAMP);
+
+ final Cursor itemCursor = resolver.query(uri, null, null, null, null);
+ try {
+ if (!itemCursor.moveToFirst()) {
+ throw new IllegalArgumentException("Missing details for " + uri);
+ }
+ final String mimeType = getCursorString(itemCursor, DocumentColumns.MIME_TYPE);
+ final String displayName = getCursorString(itemCursor, DocumentColumns.DISPLAY_NAME);
+ final int flags = getCursorInt(itemCursor, DocumentColumns.FLAGS)
+ & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
+
+ return new Document(uri, mimeType, displayName, lastModified, flags);
+ } finally {
+ itemCursor.close();
+ }
+ }
+
+ public static Document fromUri(ContentResolver resolver, Uri uri) {
+ final Cursor cursor = resolver.query(uri, null, null, null, null);
+ try {
+ if (!cursor.moveToFirst()) {
+ throw new IllegalArgumentException("Missing details for " + uri);
+ }
+ final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
+ final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
+ final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
+ final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
+
+ return new Document(uri, mimeType, displayName, lastModified, flags);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static Document fromSearch(Uri relatedUri, String query) {
+ final Uri uri = DocumentsContract.buildSearchUri(relatedUri, query);
+ final String mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
+ final String displayName = query;
+ final long lastModified = System.currentTimeMillis();
+ final int flags = 0;
+ return new Document(uri, mimeType, displayName, lastModified, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "Document{name=" + displayName + ", uri=" + uri + "}";
+ }
+
+ public boolean isCreateSupported() {
+ return (flags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
+ }
+
+ public boolean isSearchSupported() {
+ return (flags & DocumentsContract.FLAG_SUPPORTS_SEARCH) != 0;
+ }
+
+ public boolean isThumbnailSupported() {
+ return (flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0;
+ }
+
+ private static String getCursorString(Cursor cursor, String columnName) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(columnName));
+ }
+
+ private static long getCursorLong(Cursor cursor, String columnName) {
+ return cursor.getLong(cursor.getColumnIndexOrThrow(columnName));
+ }
+
+ private static int getCursorInt(Cursor cursor, String columnName) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(columnName));
+ }
+
+ public static class NameComparator implements Comparator<Document> {
+ @Override
+ public int compare(Document lhs, Document rhs) {
+ final boolean leftDir = DocumentsContract.MIME_TYPE_DIRECTORY.equals(lhs.mimeType);
+ final boolean rightDir = DocumentsContract.MIME_TYPE_DIRECTORY.equals(rhs.mimeType);
+
+ if (leftDir != rightDir) {
+ return leftDir ? -1 : 1;
+ } else {
+ return lhs.displayName.compareToIgnoreCase(rhs.displayName);
+ }
+ }
+ }
+
+ public static class DateComparator implements Comparator<Document> {
+ @Override
+ public int compare(Document lhs, Document rhs) {
+ return Long.compare(rhs.lastModified, lhs.lastModified);
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java
new file mode 100644
index 0000000..96eb58e
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java
@@ -0,0 +1,121 @@
+/*
+ * 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.model;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.provider.DocumentsContract;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.documentsui.DocumentsActivity;
+import com.google.android.collect.Lists;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Representation of a storage backend.
+ */
+public class DocumentsProviderInfo {
+ private static final String TAG = DocumentsActivity.TAG;
+
+ public ProviderInfo providerInfo;
+ public boolean customRoots;
+ public List<Icon> customIcons;
+
+ public static class Icon {
+ public String mimeType;
+ public Drawable icon;
+ }
+
+ private static final String TAG_DOCUMENTS_PROVIDER = "documents-provider";
+ private static final String TAG_ICON = "icon";
+
+ public static DocumentsProviderInfo buildRecents(Context context, ProviderInfo providerInfo) {
+ final DocumentsProviderInfo info = new DocumentsProviderInfo();
+ info.providerInfo = providerInfo;
+ info.customRoots = false;
+ return info;
+ }
+
+ public static DocumentsProviderInfo parseInfo(Context context, ProviderInfo providerInfo) {
+ final DocumentsProviderInfo info = new DocumentsProviderInfo();
+ info.providerInfo = providerInfo;
+ info.customIcons = Lists.newArrayList();
+
+ final PackageManager pm = context.getPackageManager();
+ final Resources res;
+ try {
+ res = pm.getResourcesForApplication(providerInfo.applicationInfo);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Failed to find resources for " + providerInfo, e);
+ return null;
+ }
+
+ XmlResourceParser parser = null;
+ try {
+ parser = providerInfo.loadXmlMetaData(
+ pm, DocumentsContract.META_DATA_DOCUMENT_PROVIDER);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type = 0;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ final String tag = parser.getName();
+ if (type == XmlPullParser.START_TAG && TAG_DOCUMENTS_PROVIDER.equals(tag)) {
+ final TypedArray a = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.DocumentsProviderInfo);
+ info.customRoots = a.getBoolean(
+ com.android.internal.R.styleable.DocumentsProviderInfo_customRoots,
+ false);
+ a.recycle();
+
+ } else if (type == XmlPullParser.START_TAG && TAG_ICON.equals(tag)) {
+ final TypedArray a = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.Icon);
+ final Icon icon = new Icon();
+ icon.mimeType = a.getString(com.android.internal.R.styleable.Icon_mimeType);
+ icon.icon = a.getDrawable(com.android.internal.R.styleable.Icon_icon);
+ info.customIcons.add(icon);
+ a.recycle();
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to parse metadata", e);
+ return null;
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "Failed to parse metadata", e);
+ return null;
+ } finally {
+ IoUtils.closeQuietly(parser);
+ }
+
+ return info;
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
new file mode 100644
index 0000000..ef3b8d7
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
@@ -0,0 +1,92 @@
+/*
+ * 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.model;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.RootColumns;
+
+import com.android.documentsui.R;
+import com.android.documentsui.RecentsProvider;
+
+/**
+ * Representation of a root under a storage backend.
+ */
+public class Root {
+ public String rootId;
+ public int rootType;
+ public Uri uri;
+ public Drawable icon;
+ public String title;
+ public String summary;
+ public boolean isRecents;
+
+ public static Root buildRecentOpen(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ final Root root = new Root();
+ root.rootId = null;
+ root.rootType = DocumentsContract.ROOT_TYPE_SHORTCUT;
+ root.uri = RecentsProvider.buildRecentOpen();
+ root.icon = context.getResources().getDrawable(R.drawable.ic_dir);
+ root.title = context.getString(R.string.root_recent);
+ root.summary = null;
+ root.isRecents = true;
+ return root;
+ }
+
+ public static Root fromCursor(
+ Context context, DocumentsProviderInfo info, Cursor cursor) {
+ final PackageManager pm = context.getPackageManager();
+
+ final Root root = new Root();
+ root.rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
+ root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
+ root.uri = DocumentsContract.buildDocumentUri(
+ info.providerInfo.authority, root.rootId, DocumentsContract.ROOT_DOC_ID);
+ root.icon = info.providerInfo.loadIcon(pm);
+ root.title = info.providerInfo.loadLabel(pm).toString();
+ root.summary = null;
+
+ final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
+ if (icon != 0) {
+ try {
+ root.icon = pm.getResourcesForApplication(info.providerInfo.applicationInfo)
+ .getDrawable(icon);
+ } catch (NotFoundException e) {
+ throw new RuntimeException(e);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ final String title = cursor.getString(cursor.getColumnIndex(RootColumns.TITLE));
+ if (title != null) {
+ root.title = title;
+ }
+
+ root.summary = cursor.getString(cursor.getColumnIndex(RootColumns.SUMMARY));
+ root.isRecents = false;
+
+ return root;
+ }
+}