method public static boolean isExternalStorageRemovable();
field public static java.lang.String DIRECTORY_ALARMS;
field public static java.lang.String DIRECTORY_DCIM;
+ field public static java.lang.String DIRECTORY_DOCUMENTS;
field public static java.lang.String DIRECTORY_DOWNLOADS;
field public static java.lang.String DIRECTORY_MOVIES;
field public static java.lang.String DIRECTORY_MUSIC;
field public static final java.lang.String COLUMN_MIME_TYPE = "mime_type";
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_PREFERS_GRID = 16; // 0x10
+ field public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 32; // 0x20
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
field public static final int FLAG_SUPPORTS_THUMBNAIL = 1; // 0x1
field public static final int FLAG_SUPPORTS_WRITE = 2; // 0x2
field public static final java.lang.String COLUMN_SUMMARY = "summary";
field public static final java.lang.String COLUMN_TITLE = "title";
field public static final int FLAG_ADVANCED = 4; // 0x4
+ field public static final int FLAG_EMPTY = 32; // 0x20
field public static final int FLAG_LOCAL_ONLY = 2; // 0x2
field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
field public static final int FLAG_SUPPORTS_RECENTS = 8; // 0x8
+ field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10
field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
* top-level public directory, as this convention makes no sense elsewhere.
*/
public static String DIRECTORY_DCIM = "DCIM";
-
+
+ /**
+ * Standard directory in which to place documents that have been created by
+ * the user.
+ */
+ public static String DIRECTORY_DOCUMENTS = "Documents";
+
/**
* Get a top-level public external storage directory for placing files of
* a particular type. This is where the user will typically place and
// content://com.example/root/
// content://com.example/root/sdcard/
// content://com.example/root/sdcard/recent/
+ // content://com.example/root/sdcard/search/?query=pony
// content://com.example/document/12/
// content://com.example/document/12/children/
- // content://com.example/document/12/search/?query=pony
private DocumentsContract() {
}
* @see #FLAG_SUPPORTS_THUMBNAIL
* @see #FLAG_DIR_PREFERS_GRID
* @see #FLAG_DIR_SUPPORTS_CREATE
- * @see #FLAG_DIR_SUPPORTS_SEARCH
*/
public static final String COLUMN_FLAGS = "flags";
public static final int FLAG_DIR_SUPPORTS_CREATE = 1 << 3;
/**
- * Flag indicating that a directory supports search. Only valid when
- * {@link #COLUMN_MIME_TYPE} is {@link #MIME_TYPE_DIR}.
- *
- * @see #COLUMN_FLAGS
- * @see DocumentsProvider#querySearchDocuments(String, String,
- * String[])
- */
- public static final int FLAG_DIR_SUPPORTS_SEARCH = 1 << 4;
-
- /**
* Flag indicating that a directory prefers its contents be shown in a
* larger format grid. Usually suitable when a directory contains mostly
* pictures. Only valid when {@link #COLUMN_MIME_TYPE} is
*
* @see #COLUMN_FLAGS
*/
- public static final int FLAG_DIR_PREFERS_GRID = 1 << 5;
+ public static final int FLAG_DIR_PREFERS_GRID = 1 << 4;
/**
* Flag indicating that a directory prefers its contents be sorted by
*
* @see #COLUMN_FLAGS
*/
- public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 6;
+ public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 5;
}
/**
* <p>
* Type: INTEGER (int)
*
+ * @see #FLAG_ADVANCED
+ * @see #FLAG_EMPTY
* @see #FLAG_LOCAL_ONLY
* @see #FLAG_SUPPORTS_CREATE
- * @see #FLAG_ADVANCED
+ * @see #FLAG_SUPPORTS_RECENTS
+ * @see #FLAG_SUPPORTS_SEARCH
*/
public static final String COLUMN_FLAGS = "flags";
* @see DocumentsContract#buildRecentDocumentsUri(String, String)
*/
public static final int FLAG_SUPPORTS_RECENTS = 1 << 3;
+
+ /**
+ * Flag indicating that this root supports search.
+ *
+ * @see #COLUMN_FLAGS
+ * @see DocumentsProvider#querySearchDocuments(String, String,
+ * String[])
+ */
+ public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
+
+ /**
+ * Flag indicating that this root is currently empty. This may be used
+ * to hide the root when opening documents, but the root will still be
+ * shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is
+ * also set.
+ *
+ * @see #COLUMN_FLAGS
+ * @see DocumentsProvider#querySearchDocuments(String, String,
+ * String[])
+ */
+ public static final int FLAG_EMPTY = 1 << 5;
}
/**
}
/**
- * Build Uri representing the recently modified documents of a specific
- * root. When queried, a provider will return zero or more rows with columns
- * defined by {@link Document}.
+ * Build Uri representing the recently modified documents of a specific root
+ * in a document provider. When queried, a provider will return zero or more
+ * rows with columns defined by {@link Document}.
*
* @see DocumentsProvider#queryRecentDocuments(String, String[])
* @see #getRootId(Uri)
/**
* Build Uri representing a search for matching documents under a specific
- * directory in a document provider. When queried, a provider will return
- * zero or more rows with columns defined by {@link Document}.
+ * root in a document provider. When queried, a provider will return zero or
+ * more rows with columns defined by {@link Document}.
*
- * @param parentDocumentId the document to return children for, which must
- * be both a directory with MIME type of
- * {@link Document#MIME_TYPE_DIR} and have
- * {@link Document#FLAG_DIR_SUPPORTS_SEARCH} set.
* @see DocumentsProvider#querySearchDocuments(String, String, String[])
- * @see #getDocumentId(Uri)
+ * @see #getRootId(Uri)
* @see #getSearchDocumentsQuery(Uri)
*/
public static Uri buildSearchDocumentsUri(
- String authority, String parentDocumentId, String query) {
+ String authority, String rootId, String query) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
- .appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_SEARCH)
+ .appendPath(PATH_ROOT).appendPath(rootId).appendPath(PATH_SEARCH)
.appendQueryParameter(PARAM_QUERY, query).build();
}
private static final int MATCH_ROOTS = 1;
private static final int MATCH_ROOT = 2;
private static final int MATCH_RECENT = 3;
- private static final int MATCH_DOCUMENT = 4;
- private static final int MATCH_CHILDREN = 5;
- private static final int MATCH_SEARCH = 6;
+ private static final int MATCH_SEARCH = 4;
+ private static final int MATCH_DOCUMENT = 5;
+ private static final int MATCH_CHILDREN = 6;
private String mAuthority;
mMatcher.addURI(mAuthority, "root", MATCH_ROOTS);
mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT);
mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT);
+ mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
- mMatcher.addURI(mAuthority, "document/*/search", MATCH_SEARCH);
// Sanity check our setup
if (!info.exported) {
}
/**
- * Return documents that that match the given query, starting the search at
- * the given directory.
+ * Return documents that that match the given query.
*
- * @param parentDocumentId the directory to start search at.
+ * @param rootId the root to search under.
*/
@SuppressWarnings("unused")
- public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection)
+ public Cursor querySearchDocuments(String rootId, String query, String[] projection)
throws FileNotFoundException {
throw new UnsupportedOperationException("Search not supported");
}
return queryRoots(projection);
case MATCH_RECENT:
return queryRecentDocuments(getRootId(uri), projection);
+ case MATCH_SEARCH:
+ return querySearchDocuments(
+ getRootId(uri), getSearchDocumentsQuery(uri), projection);
case MATCH_DOCUMENT:
return queryDocument(getDocumentId(uri), projection);
case MATCH_CHILDREN:
} else {
return queryChildDocuments(getDocumentId(uri), projection, sortOrder);
}
- case MATCH_SEARCH:
- return querySearchDocuments(
- getDocumentId(uri), getSearchDocumentsQuery(uri), projection);
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
show(fm, TYPE_NORMAL, root, doc, null);
}
- public static void showSearch(
- FragmentManager fm, RootInfo root, DocumentInfo doc, String query) {
- show(fm, TYPE_SEARCH, root, doc, query);
+ public static void showSearch(FragmentManager fm, RootInfo root, String query) {
+ show(fm, TYPE_SEARCH, root, null, query);
}
public static void showRecentsOpen(FragmentManager fm) {
context, mType, root, doc, contentsUri, state.userSortOrder);
case TYPE_SEARCH:
contentsUri = DocumentsContract.buildSearchDocumentsUri(
- doc.authority, doc.documentId, query);
+ root.authority, root.rootId, query);
if (state.action == ACTION_MANAGE) {
contentsUri = DocumentsContract.setManageMode(contentsUri);
}
final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
- if (root != null) {
+ if (root != null && doc != null) {
final Uri stateUri = RecentsProvider.buildState(
root.authority, root.rootId, doc.documentId);
final ContentValues values = new ContentValues();
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
+import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import libcore.io.IoUtils;
+import java.io.FileNotFoundException;
+
class DirectoryResult implements AutoCloseable {
ContentProviderClient client;
Cursor cursor;
private final int mType;
private final RootInfo mRoot;
- private final DocumentInfo mDoc;
+ private DocumentInfo mDoc;
private final Uri mUri;
private final int mUserSortOrder;
int userMode = State.MODE_UNKNOWN;
+ // Use default document when searching
+ if (mType == DirectoryFragment.TYPE_SEARCH) {
+ final Uri docUri = DocumentsContract.buildDocumentUri(
+ mRoot.authority, mRoot.documentId);
+ try {
+ mDoc = DocumentInfo.fromUri(resolver, docUri);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed to query", e);
+ result.exception = e;
+ return result;
+ }
+ }
+
// Pick up any custom modes requested by user
Cursor cursor = null;
try {
result.cursor = cursor;
} catch (Exception e) {
- Log.d(TAG, "Failed to query", e);
+ Log.w(TAG, "Failed to query", e);
result.exception = e;
ContentProviderClient.closeQuietly(result.client);
} finally {
import android.os.Bundle;
import android.os.Parcel;
import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
super.onPrepareOptionsMenu(menu);
final FragmentManager fm = getFragmentManager();
+
+ final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
} else {
createDir.setVisible(false);
- searchVisible = cwd != null && cwd.isSearchSupported();
+
+ searchVisible = root != null
+ && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0);
}
// TODO: close any search in-progress when hiding
} else {
if (mState.currentSearch != null) {
// Ongoing search
- DirectoryFragment.showSearch(fm, root, cwd, mState.currentSearch);
+ DirectoryFragment.showSearch(fm, root, mState.currentSearch);
} else {
// Normal boring directory
DirectoryFragment.showNormal(fm, root, cwd);
final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
+ final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
// Exclude read-only devices when creating
if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
if (!state.showAdvanced && advanced) continue;
// Exclude non-local devices when local only
if (state.localOnly && !localOnly) continue;
+ // Only show empty roots when creating
+ if (state.action != State.ACTION_CREATE && empty) continue;
// Only include roots that serve requested content
final boolean overlap =
return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
}
- public boolean isSearchSupported() {
- return (flags & Document.FLAG_DIR_SUPPORTS_SEARCH) != 0;
- }
-
public boolean isThumbnailSupported() {
return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
}
// TODO: remove these special case icons
if ("com.android.externalstorage.documents".equals(authority)) {
- derivedIcon = R.drawable.ic_root_sdcard;
+ if ("documents".equals(rootId)) {
+ derivedIcon = R.drawable.ic_doc_text;
+ } else {
+ 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)) {
+ if ("images_root".equals(rootId)) {
derivedIcon = R.drawable.ic_doc_image;
- } else if ("audio".equals(rootId)) {
+ } else if ("videos_root".equals(rootId)) {
+ derivedIcon = R.drawable.ic_doc_video;
+ } else if ("audio_root".equals(rootId)) {
derivedIcon = R.drawable.ic_doc_audio;
}
}
-->
<resources>
+ <!-- Title of the external storage application [CHAR LIMIT=32] -->
<string name="app_label">External Storage</string>
+
+ <!-- Title for documents backend that offers internal storage. [CHAR LIMIT=24] -->
<string name="root_internal_storage">Internal storage</string>
+ <!-- Title for documents backend that offers documents. [CHAR LIMIT=24] -->
+ <string name="root_documents">Documents</string>
</resources>
public String rootId;
public int rootType;
public int flags;
- public int icon;
public String title;
public String docId;
}
mIdToPath.put(rootId, path);
final RootInfo root = new RootInfo();
- root.rootId = "primary";
+ root.rootId = rootId;
root.rootType = Root.ROOT_TYPE_DEVICE;
- root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED;
+ root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
+ | Root.FLAG_SUPPORTS_SEARCH;
root.title = getContext().getString(R.string.root_internal_storage);
root.docId = getDocIdForFile(path);
mRoots.add(root);
throw new IllegalStateException(e);
}
+ try {
+ final String rootId = "documents";
+ final File path = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOCUMENTS);
+ mIdToPath.put(rootId, path);
+
+ final RootInfo root = new RootInfo();
+ root.rootId = rootId;
+ root.rootType = Root.ROOT_TYPE_SHORTCUT;
+ root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY
+ | Root.FLAG_SUPPORTS_SEARCH;
+ root.title = getContext().getString(R.string.root_documents);
+ root.docId = getDocIdForFile(path);
+ mRoots.add(root);
+ mIdToRoot.put(rootId, root);
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+
return true;
}
if (target == null) {
throw new FileNotFoundException("No root for " + tag);
}
+ if (!target.exists()) {
+ target.mkdirs();
+ }
target = new File(target, path);
if (!target.exists()) {
throw new FileNotFoundException("Missing file for " + docId + " at " + target);
int flags = 0;
- if (file.isDirectory()) {
- flags |= Document.FLAG_DIR_SUPPORTS_SEARCH;
- }
if (file.isDirectory() && file.canWrite()) {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
}
row.add(Root.COLUMN_ROOT_ID, root.rootId);
row.add(Root.COLUMN_ROOT_TYPE, root.rootType);
row.add(Root.COLUMN_FLAGS, root.flags);
- row.add(Root.COLUMN_ICON, root.icon);
row.add(Root.COLUMN_TITLE, root.title);
row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
row.add(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace());
}
@Override
- public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection)
+ public Cursor querySearchDocuments(String rootId, String query, String[] projection)
throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
- final File parent = getFileForDocId(parentDocumentId);
+ final File parent = mIdToPath.get(rootId);
final LinkedList<File> pending = new LinkedList<File>();
pending.add(parent);