am 9fd81a90: Merge "Stronger DocumentsProvider contract." into klp-dev
* commit '9fd81a9008d5c8dd33272b6a451d89fa2fa1841e':
Stronger DocumentsProvider contract.
diff --git a/api/current.txt b/api/current.txt
index 6a83508..0418cd9 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20814,40 +20814,46 @@
}
public final class DocumentsContract {
- ctor public DocumentsContract();
- method public static android.net.Uri buildContentsUri(java.lang.String, java.lang.String, java.lang.String);
- method public static android.net.Uri buildContentsUri(android.net.Uri);
- method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String, java.lang.String);
- method public static android.net.Uri buildDocumentUri(android.net.Uri, java.lang.String);
- method public static android.net.Uri buildRootUri(java.lang.String, java.lang.String);
- method public static android.net.Uri buildRootsUri(java.lang.String);
- method public static android.net.Uri buildSearchUri(java.lang.String, java.lang.String, java.lang.String, java.lang.String);
- method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
- method public static android.net.Uri createDocument(android.content.ContentResolver, android.net.Uri, java.lang.String, java.lang.String);
+ method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
method public static java.lang.String getDocId(android.net.Uri);
method public static android.net.Uri[] getOpenDocuments(android.content.Context);
- method public static java.lang.String getRootId(android.net.Uri);
- method public static java.lang.String getSearchQuery(android.net.Uri);
- method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
- method public static boolean isLocalOnly(android.net.Uri);
- method public static void notifyRootsChanged(android.content.Context, java.lang.String);
- method public static boolean renameDocument(android.content.ContentResolver, android.net.Uri, java.lang.String);
- method public static android.net.Uri setLocalOnly(android.net.Uri);
- field public static final java.lang.String EXTRA_HAS_MORE = "has_more";
- field public static final java.lang.String EXTRA_REQUEST_MORE = "request_more";
- field public static final java.lang.String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+ field public static final java.lang.String EXTRA_ERROR = "error";
+ field public static final java.lang.String EXTRA_INFO = "info";
+ field public static final java.lang.String EXTRA_LOADING = "loading";
}
public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
field public static final java.lang.String DOC_ID = "doc_id";
field public static final java.lang.String FLAGS = "flags";
+ field public static final java.lang.String ICON = "icon";
field public static final java.lang.String LAST_MODIFIED = "last_modified";
field public static final java.lang.String MIME_TYPE = "mime_type";
field public static final java.lang.String SUMMARY = "summary";
}
- public static class DocumentsContract.Documents {
- field public static final java.lang.String DOC_ID_ROOT = "0";
+ public static final class DocumentsContract.DocumentRoot implements android.os.Parcelable {
+ ctor public DocumentsContract.DocumentRoot();
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ 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 ROOT_TYPE_DEVICE = 3; // 0x3
+ field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
+ field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
+ field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
+ field public long availableBytes;
+ field public java.lang.String docId;
+ field public int flags;
+ field public int icon;
+ field public java.lang.String[] mimeTypes;
+ field public java.lang.String recentDocId;
+ field public int rootType;
+ field public java.lang.String summary;
+ field public java.lang.String title;
+ }
+
+ public static final class DocumentsContract.Documents {
field public static final int FLAG_PREFERS_GRID = 64; // 0x40
field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4
@@ -20855,25 +20861,32 @@
field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10
field public static final int FLAG_SUPPORTS_THUMBNAIL = 8; // 0x8
field public static final int FLAG_SUPPORTS_WRITE = 32; // 0x20
- field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
+ field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.doc/dir";
}
- public static abstract interface DocumentsContract.RootColumns {
- field public static final java.lang.String AVAILABLE_BYTES = "available_bytes";
- field public static final java.lang.String ICON = "icon";
- field public static final java.lang.String ROOT_ID = "root_id";
- field public static final java.lang.String ROOT_TYPE = "root_type";
- field public static final java.lang.String SUMMARY = "summary";
- field public static final java.lang.String TITLE = "title";
- }
-
- public static class DocumentsContract.Roots {
- field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
- field public static final java.lang.String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
- field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
- field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
- field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
- field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
+ public abstract class DocumentsProvider extends android.content.ContentProvider {
+ ctor public DocumentsProvider();
+ method public final android.os.Bundle callFromPackage(java.lang.String, java.lang.String, java.lang.String, android.os.Bundle);
+ method public java.lang.String createDocument(java.lang.String, java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
+ method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
+ method public void deleteDocument(java.lang.String) throws java.io.FileNotFoundException;
+ method public abstract java.util.List<android.provider.DocumentsContract.DocumentRoot> getDocumentRoots();
+ method public java.lang.String getType(java.lang.String) throws java.io.FileNotFoundException;
+ method public final java.lang.String getType(android.net.Uri);
+ method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
+ method public void notifyDocumentRootsChanged();
+ method public abstract android.os.ParcelFileDescriptor openDocument(java.lang.String, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method public android.content.res.AssetFileDescriptor openDocumentThumbnail(java.lang.String, android.graphics.Point, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
+ method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException;
+ method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String);
+ method public abstract android.database.Cursor queryDocument(java.lang.String) throws java.io.FileNotFoundException;
+ method public abstract android.database.Cursor queryDocumentChildren(java.lang.String) throws java.io.FileNotFoundException;
+ method public android.database.Cursor querySearch(java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
+ method public void renameDocument(java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
+ method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
}
public final deprecated class LiveFolders implements android.provider.BaseColumns {
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 024a521..4e8dd82 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -316,4 +316,11 @@
public ContentProvider getLocalContentProvider() {
return ContentProvider.coerceToLocalContentProvider(mContentProvider);
}
+
+ /** {@hide} */
+ public static void closeQuietly(ContentProviderClient client) {
+ if (client != null) {
+ client.release();
+ }
+ }
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index c99f09c..d7ca915 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2687,10 +2687,6 @@
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
- /** {@hide} */
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_MANAGE_DOCUMENT = "android.intent.action.MANAGE_DOCUMENT";
-
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Standard intent categories (see addCategory()).
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index f474504..32b1b60 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -22,6 +22,7 @@
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
/**
@@ -545,6 +546,13 @@
mFdsKnown = false;
}
+ /** {@hide} */
+ public void putParcelableList(String key, List<? extends Parcelable> value) {
+ unparcel();
+ mMap.put(key, value);
+ mFdsKnown = false;
+ }
+
/**
* Inserts a SparceArray of Parcelable values into the mapping of this
* Bundle, replacing any existing value for the given key. Either key
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 65c9220..ebb7eb8 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -19,9 +19,8 @@
import static android.net.TrafficStats.KB_IN_BYTES;
import static libcore.io.OsConstants.SEEK_SET;
-import android.content.ContentProvider;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -31,12 +30,16 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.os.Parcelable;
import android.util.Log;
+import com.android.internal.util.Preconditions;
import com.google.android.collect.Lists;
import libcore.io.ErrnoException;
@@ -51,74 +54,49 @@
/**
* Defines the contract between a documents provider and the platform.
* <p>
- * A document provider is a {@link ContentProvider} that presents a set of
- * documents in a hierarchical structure. The system provides UI that visualizes
- * all available document providers, offering users the ability to open existing
- * documents or create new documents.
- * <p>
- * Each provider expresses one or more "roots" which each serve as the top-level
- * of a tree. For example, a root could represent an account, or a physical
- * storage device. Under each root, documents are referenced by a unique
- * {@link DocumentColumns#DOC_ID}, and each root starts at the
- * {@link Documents#DOC_ID_ROOT} document.
- * <p>
- * Documents can be either an openable file (with a specific MIME type), or a
- * directory containing additional documents (with the
- * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
- * capabilities, as described by {@link DocumentColumns#FLAGS}. The same
- * {@link DocumentColumns#DOC_ID} can be included in multiple directories.
- * <p>
- * Document providers must be protected with the
- * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
- * only be requested by the system. The system-provided UI then issues narrow
- * Uri permission grants for individual documents when the user explicitly picks
- * documents.
+ * To create a document provider, extend {@link DocumentsProvider}, which
+ * provides a foundational implementation of this contract.
*
- * @see Intent#ACTION_OPEN_DOCUMENT
- * @see Intent#ACTION_CREATE_DOCUMENT
+ * @see DocumentsProvider
*/
public final class DocumentsContract {
private static final String TAG = "Documents";
- // content://com.example/roots/
- // content://com.example/roots/sdcard/
- // content://com.example/roots/sdcard/docs/0/
- // content://com.example/roots/sdcard/docs/0/contents/
- // content://com.example/roots/sdcard/docs/0/search/?query=pony
+ // content://com.example/docs/12/
+ // content://com.example/docs/12/children/
+ // content://com.example/docs/12/search/?query=pony
+
+ private DocumentsContract() {
+ }
/** {@hide} */
public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
/** {@hide} */
- public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED";
+ public static final String ACTION_MANAGE_DOCUMENTS = "android.provider.action.MANAGE_DOCUMENTS";
+
+ /** {@hide} */
+ public static final String
+ ACTION_DOCUMENT_ROOT_CHANGED = "android.provider.action.DOCUMENT_ROOT_CHANGED";
/**
* Constants for individual documents.
*/
- public static class Documents {
+ public final static class Documents {
private Documents() {
}
/**
* MIME type of a document which is a directory that may contain additional
* documents.
- *
- * @see #buildContentsUri(String, String, String)
*/
- public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
-
- /**
- * {@link DocumentColumns#DOC_ID} value representing the root directory of a
- * documents root.
- */
- public static final String DOC_ID_ROOT = "0";
+ public static final String MIME_TYPE_DIR = "vnd.android.doc/dir";
/**
* Flag indicating that a document is a directory that supports creation of
* new files within it.
*
* @see DocumentColumns#FLAGS
- * @see #createDocument(ContentResolver, Uri, String, String)
*/
public static final int FLAG_SUPPORTS_CREATE = 1;
@@ -126,7 +104,6 @@
* Flag indicating that a document is renamable.
*
* @see DocumentColumns#FLAGS
- * @see #renameDocument(ContentResolver, Uri, String)
*/
public static final int FLAG_SUPPORTS_RENAME = 1 << 1;
@@ -141,7 +118,6 @@
* Flag indicating that a document can be represented as a thumbnail.
*
* @see DocumentColumns#FLAGS
- * @see #getThumbnail(ContentResolver, Uri, Point)
*/
public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;
@@ -153,7 +129,7 @@
public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
/**
- * Flag indicating that a document is writable.
+ * Flag indicating that a document supports writing.
*
* @see DocumentColumns#FLAGS
*/
@@ -170,127 +146,89 @@
}
/**
- * Optimal dimensions for a document thumbnail request, stored as a
- * {@link Point} object. This is only a hint, and the returned thumbnail may
- * have different dimensions.
- *
- * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
- */
- public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
-
- /**
* Extra boolean flag included in a directory {@link Cursor#getExtras()}
- * indicating that the document provider can provide additional data if
- * requested, such as additional search results.
- */
- public static final String EXTRA_HAS_MORE = "has_more";
-
- /**
- * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
- * directory to request that additional data should be fetched. When
- * requested data is ready, the provider should send a change notification
- * to cause a requery.
+ * indicating that a document provider is still loading data. For example, a
+ * provider has returned some results, but is still waiting on an
+ * outstanding network request.
*
- * @see Cursor#respond(Bundle)
* @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
* boolean)
*/
- public static final String EXTRA_REQUEST_MORE = "request_more";
+ public static final String EXTRA_LOADING = "loading";
- private static final String PATH_ROOTS = "roots";
+ /**
+ * Extra string included in a directory {@link Cursor#getExtras()}
+ * providing an informational message that should be shown to a user. For
+ * example, a provider may wish to indicate that not all documents are
+ * available.
+ */
+ public static final String EXTRA_INFO = "info";
+
+ /**
+ * Extra string included in a directory {@link Cursor#getExtras()} providing
+ * an error message that should be shown to a user. For example, a provider
+ * may wish to indicate that a network error occurred. The user may choose
+ * to retry, resulting in a new query.
+ */
+ public static final String EXTRA_ERROR = "error";
+
+ /** {@hide} */
+ public static final String METHOD_GET_ROOTS = "android:getRoots";
+ /** {@hide} */
+ public static final String METHOD_CREATE_DOCUMENT = "android:createDocument";
+ /** {@hide} */
+ public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument";
+ /** {@hide} */
+ public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument";
+
+ /** {@hide} */
+ public static final String EXTRA_AUTHORITY = "authority";
+ /** {@hide} */
+ public static final String EXTRA_PACKAGE_NAME = "packageName";
+ /** {@hide} */
+ public static final String EXTRA_URI = "uri";
+ /** {@hide} */
+ public static final String EXTRA_ROOTS = "roots";
+ /** {@hide} */
+ public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+
private static final String PATH_DOCS = "docs";
- private static final String PATH_CONTENTS = "contents";
+ private static final String PATH_CHILDREN = "children";
private static final String PATH_SEARCH = "search";
private static final String PARAM_QUERY = "query";
- private static final String PARAM_LOCAL_ONLY = "localOnly";
-
- /**
- * Build Uri representing the roots offered by a document provider.
- */
- public static Uri buildRootsUri(String authority) {
- return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(authority).appendPath(PATH_ROOTS).build();
- }
-
- /**
- * Build Uri representing a specific root offered by a document provider.
- */
- public static Uri buildRootUri(String authority, String rootId) {
- return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
- }
/**
* Build Uri representing the given {@link DocumentColumns#DOC_ID} in a
* document provider.
*/
- public static Uri buildDocumentUri(String authority, String rootId, String docId) {
- return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
- .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
- .build();
+ public static Uri buildDocumentUri(String authority, String docId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority).appendPath(PATH_DOCS).appendPath(docId).build();
}
/**
* Build Uri representing the contents of the given directory in a document
* provider. The given document must be {@link Documents#MIME_TYPE_DIR}.
+ *
+ * @hide
*/
- public static Uri buildContentsUri(String authority, String rootId, String docId) {
+ public static Uri buildChildrenUri(String authority, String docId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
- .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
- .appendPath(PATH_CONTENTS).build();
+ .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_CHILDREN).build();
}
/**
* Build Uri representing a search for matching documents under a specific
* directory in a document provider. The given document must have
* {@link Documents#FLAG_SUPPORTS_SEARCH}.
+ *
+ * @hide
*/
- public static Uri buildSearchUri(String authority, String rootId, String docId, String query) {
+ public static Uri buildSearchUri(String authority, String docId, String query) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
- .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
- .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
- }
-
- /**
- * Convenience method for {@link #buildDocumentUri(String, String, String)},
- * extracting authority and root from the given Uri.
- */
- public static Uri buildDocumentUri(Uri relatedUri, String docId) {
- return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId);
- }
-
- /**
- * Convenience method for {@link #buildContentsUri(String, String, String)},
- * extracting authority and root from the given Uri.
- */
- public static Uri buildContentsUri(Uri relatedUri) {
- return buildContentsUri(
- relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri));
- }
-
- /**
- * Convenience method for
- * {@link #buildSearchUri(String, String, String, String)}, extracting
- * authority and root from the given Uri.
- */
- public static Uri buildSearchUri(Uri relatedUri, String query) {
- return buildSearchUri(
- relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query);
- }
-
- /**
- * Extract the {@link RootColumns#ROOT_ID} from the given Uri.
- */
- public static String getRootId(Uri documentUri) {
- final List<String> paths = documentUri.getPathSegments();
- if (paths.size() < 2) {
- throw new IllegalArgumentException("Not a root: " + documentUri);
- }
- if (!PATH_ROOTS.equals(paths.get(0))) {
- throw new IllegalArgumentException("Not a root: " + documentUri);
- }
- return paths.get(1);
+ .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_SEARCH)
+ .appendQueryParameter(PARAM_QUERY, query).build();
}
/**
@@ -298,68 +236,35 @@
*/
public static String getDocId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
- if (paths.size() < 4) {
+ if (paths.size() < 2) {
throw new IllegalArgumentException("Not a document: " + documentUri);
}
- if (!PATH_ROOTS.equals(paths.get(0))) {
+ if (!PATH_DOCS.equals(paths.get(0))) {
throw new IllegalArgumentException("Not a document: " + documentUri);
}
- if (!PATH_DOCS.equals(paths.get(2))) {
- throw new IllegalArgumentException("Not a document: " + documentUri);
- }
- return paths.get(3);
+ return paths.get(1);
}
- /**
- * Return requested search query from the given Uri, as constructed by
- * {@link #buildSearchUri(String, String, String, String)}.
- */
+ /** {@hide} */
public static String getSearchQuery(Uri documentUri) {
return documentUri.getQueryParameter(PARAM_QUERY);
}
/**
- * Mark the given Uri to indicate that only locally-available data should be
- * returned. That is, no network connections should be initiated to provide
- * the metadata or content.
- */
- public static Uri setLocalOnly(Uri documentUri) {
- return documentUri.buildUpon()
- .appendQueryParameter(PARAM_LOCAL_ONLY, String.valueOf(true)).build();
- }
-
- /**
- * Return if the given Uri is requesting that only locally-available data be
- * returned. That is, no network connections should be initiated to provide
- * the metadata or content.
- */
- public static boolean isLocalOnly(Uri documentUri) {
- return documentUri.getBooleanQueryParameter(PARAM_LOCAL_ONLY, false);
- }
-
- /**
* Standard columns for document queries. Document providers <em>must</em>
* support at least these columns when queried.
- *
- * @see DocumentsContract#buildDocumentUri(String, String, String)
- * @see DocumentsContract#buildContentsUri(String, String, String)
- * @see DocumentsContract#buildSearchUri(String, String, String, String)
*/
public interface DocumentColumns extends OpenableColumns {
/**
- * The ID for a document under a storage backend root. Values
- * <em>must</em> never change once returned. This field is read-only to
- * document clients.
+ * Unique ID for a document. Values <em>must</em> never change once
+ * returned, since they may used for long-term Uri permission grants.
* <p>
* Type: STRING
*/
public static final String DOC_ID = "doc_id";
/**
- * MIME type of a document, matching the value returned by
- * {@link ContentResolver#getType(android.net.Uri)}. This field must be
- * provided when a new document is created. This field is read-only to
- * document clients.
+ * MIME type of a document.
* <p>
* Type: STRING
*
@@ -369,10 +274,10 @@
/**
* Timestamp when a document was last modified, in milliseconds since
- * January 1, 1970 00:00:00.0 UTC. This field is read-only to document
- * clients. Document providers can update this field using events from
+ * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. Document
+ * providers can update this field using events from
* {@link OnCloseListener} or other reliable
- * {@link ParcelFileDescriptor} transport.
+ * {@link ParcelFileDescriptor} transports.
* <p>
* Type: INTEGER (long)
*
@@ -381,37 +286,37 @@
public static final String LAST_MODIFIED = "last_modified";
/**
- * Flags that apply to a specific document. This field is read-only to
- * document clients.
+ * Specific icon resource for a document, or {@code null} to resolve
+ * default using {@link #MIME_TYPE}.
* <p>
* Type: INTEGER (int)
*/
- public static final String FLAGS = "flags";
+ public static final String ICON = "icon";
/**
- * Summary for this document, or {@code null} to omit. This field is
- * read-only to document clients.
+ * Summary for a document, or {@code null} to omit.
* <p>
* Type: STRING
*/
public static final String SUMMARY = "summary";
+
+ /**
+ * Flags that apply to a specific document.
+ * <p>
+ * Type: INTEGER (int)
+ */
+ public static final String FLAGS = "flags";
}
/**
- * Constants for individual document roots.
+ * Metadata about a specific root of documents.
*/
- public static class Roots {
- private Roots() {
- }
-
- public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
- public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
-
+ public final static class DocumentRoot implements Parcelable {
/**
* Root that represents a storage service, such as a cloud-based
* service.
*
- * @see RootColumns#ROOT_TYPE
+ * @see #rootType
*/
public static final int ROOT_TYPE_SERVICE = 1;
@@ -419,14 +324,14 @@
* Root that represents a shortcut to content that may be available
* elsewhere through another storage root.
*
- * @see RootColumns#ROOT_TYPE
+ * @see #rootType
*/
public static final int ROOT_TYPE_SHORTCUT = 2;
/**
* Root that represents a physical storage device.
*
- * @see RootColumns#ROOT_TYPE
+ * @see #rootType
*/
public static final int ROOT_TYPE_DEVICE = 3;
@@ -434,65 +339,154 @@
* Root that represents a physical storage device that should only be
* displayed to advanced users.
*
- * @see RootColumns#ROOT_TYPE
+ * @see #rootType
*/
public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
- }
-
- /**
- * Standard columns for document root queries.
- *
- * @see DocumentsContract#buildRootsUri(String)
- * @see DocumentsContract#buildRootUri(String, String)
- */
- public interface RootColumns {
- public static final String ROOT_ID = "root_id";
/**
- * Storage root type, use for clustering. This field is read-only to
- * document clients.
- * <p>
- * Type: INTEGER (int)
+ * Flag indicating that at least one directory under this root supports
+ * creating content.
*
- * @see Roots#ROOT_TYPE_SERVICE
- * @see Roots#ROOT_TYPE_DEVICE
+ * @see #flags
*/
- public static final String ROOT_TYPE = "root_type";
+ public static final int FLAG_SUPPORTS_CREATE = 1;
/**
- * Icon resource ID for this storage root, or {@code null} to use the
- * default {@link ProviderInfo#icon}. This field is read-only to
- * document clients.
- * <p>
- * Type: INTEGER (int)
+ * Flag indicating that this root offers content that is strictly local
+ * on the device. That is, no network requests are made for the content.
+ *
+ * @see #flags
*/
- public static final String ICON = "icon";
+ public static final int FLAG_LOCAL_ONLY = 1 << 1;
+
+ /** {@hide} */
+ public String authority;
/**
- * Title for this storage root, or {@code null} to use the default
- * {@link ProviderInfo#labelRes}. This field is read-only to document
- * clients.
- * <p>
- * Type: STRING
+ * Root type, use for clustering.
+ *
+ * @see #ROOT_TYPE_SERVICE
+ * @see #ROOT_TYPE_DEVICE
*/
- public static final String TITLE = "title";
+ public int rootType;
/**
- * Summary for this storage root, or {@code null} to omit. This field is
- * read-only to document clients.
- * <p>
- * Type: STRING
+ * Flags for this root.
+ *
+ * @see #FLAG_LOCAL_ONLY
*/
- public static final String SUMMARY = "summary";
+ public int flags;
/**
- * Number of free bytes of available in this storage root, or
- * {@code null} if unknown or unbounded. This field is read-only to
- * document clients.
- * <p>
- * Type: INTEGER (long)
+ * Icon resource ID for this root.
*/
- public static final String AVAILABLE_BYTES = "available_bytes";
+ public int icon;
+
+ /**
+ * Title for this root.
+ */
+ public String title;
+
+ /**
+ * Summary for this root. May be {@code null}.
+ */
+ public String summary;
+
+ /**
+ * Document which is a directory that represents the top of this root.
+ * Must not be {@code null}.
+ *
+ * @see DocumentColumns#DOC_ID
+ */
+ public String docId;
+
+ /**
+ * Document which is a directory representing recently modified
+ * documents under this root. This directory should return at most two
+ * dozen documents modified within the last 90 days. May be {@code null}
+ * if this root doesn't support recents.
+ *
+ * @see DocumentColumns#DOC_ID
+ */
+ public String recentDocId;
+
+ /**
+ * Number of free bytes of available in this root, or -1 if unknown or
+ * unbounded.
+ */
+ public long availableBytes;
+
+ /**
+ * Set of MIME type filters describing the content offered by this root,
+ * or {@code null} to indicate that all MIME types are supported. For
+ * example, a provider only supporting audio and video might set this to
+ * {@code ["audio/*", "video/*"]}.
+ */
+ public String[] mimeTypes;
+
+ public DocumentRoot() {
+ }
+
+ /** {@hide} */
+ public DocumentRoot(Parcel in) {
+ rootType = in.readInt();
+ flags = in.readInt();
+ icon = in.readInt();
+ title = in.readString();
+ summary = in.readString();
+ docId = in.readString();
+ recentDocId = in.readString();
+ availableBytes = in.readLong();
+ mimeTypes = in.readStringArray();
+ }
+
+ /** {@hide} */
+ public Drawable loadIcon(Context context) {
+ if (icon != 0) {
+ if (authority != null) {
+ final PackageManager pm = context.getPackageManager();
+ final ProviderInfo info = pm.resolveContentProvider(authority, 0);
+ if (info != null) {
+ return pm.getDrawable(info.packageName, icon, info.applicationInfo);
+ }
+ } else {
+ return context.getResources().getDrawable(icon);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Preconditions.checkNotNull(docId);
+
+ dest.writeInt(rootType);
+ dest.writeInt(flags);
+ dest.writeInt(icon);
+ dest.writeString(title);
+ dest.writeString(summary);
+ dest.writeString(docId);
+ dest.writeString(recentDocId);
+ dest.writeLong(availableBytes);
+ dest.writeStringArray(mimeTypes);
+ }
+
+ public static final Creator<DocumentRoot> CREATOR = new Creator<DocumentRoot>() {
+ @Override
+ public DocumentRoot createFromParcel(Parcel in) {
+ return new DocumentRoot(in);
+ }
+
+ @Override
+ public DocumentRoot[] newArray(int size) {
+ return new DocumentRoot[size];
+ }
+ };
}
/**
@@ -531,6 +525,7 @@
* {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set.
*
* @return decoded thumbnail, or {@code null} if problem was encountered.
+ * @hide
*/
public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
final Bundle openOpts = new Bundle();
@@ -588,44 +583,83 @@
}
}
- /**
- * Create a new document under a specific parent document with the given
- * display name and MIME type.
- *
- * @param parentDocumentUri document with
- * {@link Documents#FLAG_SUPPORTS_CREATE}
- * @param displayName name for new document
- * @param mimeType MIME type for new document, which cannot be changed
- * @return newly created document Uri, or {@code null} if failed
- */
- public static Uri createDocument(
- ContentResolver resolver, Uri parentDocumentUri, String displayName, String mimeType) {
- final ContentValues values = new ContentValues();
- values.put(DocumentColumns.MIME_TYPE, mimeType);
- values.put(DocumentColumns.DISPLAY_NAME, displayName);
- return resolver.insert(parentDocumentUri, values);
+ /** {@hide} */
+ public static List<DocumentRoot> getDocumentRoots(ContentProviderClient client) {
+ try {
+ final Bundle out = client.call(METHOD_GET_ROOTS, null, null);
+ final List<DocumentRoot> roots = out.getParcelableArrayList(EXTRA_ROOTS);
+ return roots;
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to get roots", e);
+ return null;
+ }
}
/**
- * Rename the document at the given URI. Given document must have
- * {@link Documents#FLAG_SUPPORTS_RENAME} set.
+ * Create a new document under the given parent document with MIME type and
+ * display name.
*
- * @return if rename was successful.
+ * @param docId document with {@link Documents#FLAG_SUPPORTS_CREATE}
+ * @param mimeType MIME type of new document
+ * @param displayName name of new document
+ * @return newly created document, or {@code null} if failed
+ * @hide
*/
- public static boolean renameDocument(
- ContentResolver resolver, Uri documentUri, String displayName) {
- final ContentValues values = new ContentValues();
- values.put(DocumentColumns.DISPLAY_NAME, displayName);
- return (resolver.update(documentUri, values, null, null) == 1);
+ public static String createDocument(
+ ContentProviderClient client, String docId, String mimeType, String displayName) {
+ final Bundle in = new Bundle();
+ in.putString(DocumentColumns.DOC_ID, docId);
+ in.putString(DocumentColumns.MIME_TYPE, mimeType);
+ in.putString(DocumentColumns.DISPLAY_NAME, displayName);
+
+ try {
+ final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
+ return out.getString(DocumentColumns.DOC_ID);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to create document", e);
+ return null;
+ }
}
/**
- * Notify the system that roots have changed for the given storage provider.
- * This signal is used to invalidate internal caches.
+ * Rename the given document.
+ *
+ * @param docId document with {@link Documents#FLAG_SUPPORTS_RENAME}
+ * @return document which may have changed due to rename, or {@code null} if
+ * rename failed.
+ * @hide
*/
- public static void notifyRootsChanged(Context context, String authority) {
- final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED);
- intent.setData(buildRootsUri(authority));
- context.sendBroadcast(intent);
+ public static String renameDocument(
+ ContentProviderClient client, String docId, String displayName) {
+ final Bundle in = new Bundle();
+ in.putString(DocumentColumns.DOC_ID, docId);
+ in.putString(DocumentColumns.DISPLAY_NAME, displayName);
+
+ try {
+ final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in);
+ return out.getString(DocumentColumns.DOC_ID);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to rename document", e);
+ return null;
+ }
+ }
+
+ /**
+ * Delete the given document.
+ *
+ * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE}
+ * @hide
+ */
+ public static boolean deleteDocument(ContentProviderClient client, String docId) {
+ final Bundle in = new Bundle();
+ in.putString(DocumentColumns.DOC_ID, docId);
+
+ try {
+ client.call(METHOD_DELETE_DOCUMENT, null, in);
+ return true;
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to delete document", e);
+ return false;
+ }
}
}
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
new file mode 100644
index 0000000..eeb8c41
--- /dev/null
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -0,0 +1,384 @@
+/*
+ * 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 android.provider;
+
+import static android.provider.DocumentsContract.ACTION_DOCUMENT_ROOT_CHANGED;
+import static android.provider.DocumentsContract.EXTRA_AUTHORITY;
+import static android.provider.DocumentsContract.EXTRA_ROOTS;
+import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE;
+import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
+import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
+import static android.provider.DocumentsContract.METHOD_GET_ROOTS;
+import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT;
+import static android.provider.DocumentsContract.getDocId;
+import static android.provider.DocumentsContract.getSearchQuery;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.DocumentRoot;
+import android.provider.DocumentsContract.Documents;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.FileNotFoundException;
+import java.util.List;
+
+/**
+ * Base class for a document provider. A document provider should extend this
+ * class and implement the abstract methods.
+ * <p>
+ * Each document provider expresses one or more "roots" which each serve as the
+ * top-level of a tree. For example, a root could represent an account, or a
+ * physical storage device. Under each root, documents are referenced by
+ * {@link DocumentColumns#DOC_ID}, which must not change once returned.
+ * <p>
+ * Documents can be either an openable file (with a specific MIME type), or a
+ * directory containing additional documents (with the
+ * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
+ * capabilities, as described by {@link DocumentColumns#FLAGS}. The same
+ * {@link DocumentColumns#DOC_ID} can be included in multiple directories.
+ * <p>
+ * Document providers must be protected with the
+ * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
+ * only be requested by the system. The system-provided UI then issues narrow
+ * Uri permission grants for individual documents when the user explicitly picks
+ * documents.
+ *
+ * @see Intent#ACTION_OPEN_DOCUMENT
+ * @see Intent#ACTION_CREATE_DOCUMENT
+ */
+public abstract class DocumentsProvider extends ContentProvider {
+ private static final String TAG = "DocumentsProvider";
+
+ private static final int MATCH_DOCUMENT = 1;
+ private static final int MATCH_CHILDREN = 2;
+ private static final int MATCH_SEARCH = 3;
+
+ private String mAuthority;
+
+ private UriMatcher mMatcher;
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ mAuthority = info.authority;
+
+ mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mMatcher.addURI(mAuthority, "docs/*", MATCH_DOCUMENT);
+ mMatcher.addURI(mAuthority, "docs/*/children", MATCH_CHILDREN);
+ mMatcher.addURI(mAuthority, "docs/*/search", MATCH_SEARCH);
+
+ // Sanity check our setup
+ if (!info.exported) {
+ throw new SecurityException("Provider must be exported");
+ }
+ if (!info.grantUriPermissions) {
+ throw new SecurityException("Provider must grantUriPermissions");
+ }
+ if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
+ || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
+ throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
+ }
+
+ super.attachInfo(context, info);
+ }
+
+ /**
+ * Return list of all document roots provided by this document provider.
+ * When this list changes, a provider must call
+ * {@link #notifyDocumentRootsChanged()}.
+ */
+ public abstract List<DocumentRoot> getDocumentRoots();
+
+ /**
+ * Create and return a new document. A provider must allocate a new
+ * {@link DocumentColumns#DOC_ID} to represent the document, which must not
+ * change once returned.
+ *
+ * @param docId the parent directory to create the new document under.
+ * @param mimeType the MIME type associated with the new document.
+ * @param displayName the display name of the new document.
+ */
+ @SuppressWarnings("unused")
+ public String createDocument(String docId, String mimeType, String displayName)
+ throws FileNotFoundException {
+ throw new UnsupportedOperationException("Create not supported");
+ }
+
+ /**
+ * Rename the given document.
+ *
+ * @param docId the document to rename.
+ * @param displayName the new display name.
+ */
+ @SuppressWarnings("unused")
+ public void renameDocument(String docId, String displayName) throws FileNotFoundException {
+ throw new UnsupportedOperationException("Rename not supported");
+ }
+
+ /**
+ * Delete the given document.
+ *
+ * @param docId the document to delete.
+ */
+ @SuppressWarnings("unused")
+ public void deleteDocument(String docId) throws FileNotFoundException {
+ throw new UnsupportedOperationException("Delete not supported");
+ }
+
+ /**
+ * Return metadata for the given document. A provider should avoid making
+ * network requests to keep this request fast.
+ *
+ * @param docId the document to return.
+ */
+ public abstract Cursor queryDocument(String docId) throws FileNotFoundException;
+
+ /**
+ * Return the children of the given document which is a directory.
+ *
+ * @param docId the directory to return children for.
+ */
+ public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException;
+
+ /**
+ * Return documents that that match the given query, starting the search at
+ * the given directory.
+ *
+ * @param docId the directory to start search at.
+ */
+ @SuppressWarnings("unused")
+ public Cursor querySearch(String docId, String query) throws FileNotFoundException {
+ throw new UnsupportedOperationException("Search not supported");
+ }
+
+ /**
+ * Return MIME type for the given document. Must match the value of
+ * {@link DocumentColumns#MIME_TYPE} for this document.
+ */
+ public String getType(String docId) throws FileNotFoundException {
+ final Cursor cursor = queryDocument(docId);
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE));
+ } else {
+ return null;
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ }
+ }
+
+ /**
+ * Open and return the requested document. A provider should return a
+ * reliable {@link ParcelFileDescriptor} to detect when the remote caller
+ * has finished reading or writing the document. A provider may return a
+ * pipe or socket pair if the mode is exclusively
+ * {@link ParcelFileDescriptor#MODE_READ_ONLY} or
+ * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
+ * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
+ * disk. If a provider blocks while downloading content, it should
+ * periodically check {@link CancellationSignal#isCanceled()} to abort
+ * abandoned open requests.
+ *
+ * @param docId the document to return.
+ * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
+ * @param signal used by the caller to signal if the request should be
+ * cancelled.
+ * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
+ * OnCloseListener)
+ * @see ParcelFileDescriptor#createReliablePipe()
+ * @see ParcelFileDescriptor#createReliableSocketPair()
+ */
+ public abstract ParcelFileDescriptor openDocument(
+ String docId, String mode, CancellationSignal signal) throws FileNotFoundException;
+
+ /**
+ * Open and return a thumbnail of the requested document. A provider should
+ * return a thumbnail closely matching the hinted size, attempting to serve
+ * from a local cache if possible. A provider should never return images
+ * more than double the hinted size. If a provider performs expensive
+ * operations to download or generate a thumbnail, it should periodically
+ * check {@link CancellationSignal#isCanceled()} to abort abandoned
+ * thumbnail requests.
+ *
+ * @param docId the document to return.
+ * @param sizeHint hint of the optimal thumbnail dimensions.
+ * @param signal used by the caller to signal if the request should be
+ * cancelled.
+ * @see Documents#FLAG_SUPPORTS_THUMBNAIL
+ */
+ @SuppressWarnings("unused")
+ public AssetFileDescriptor openDocumentThumbnail(
+ String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+ throw new UnsupportedOperationException("Thumbnails not supported");
+ }
+
+ @Override
+ public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ try {
+ switch (mMatcher.match(uri)) {
+ case MATCH_DOCUMENT:
+ return queryDocument(getDocId(uri));
+ case MATCH_CHILDREN:
+ return queryDocumentChildren(getDocId(uri));
+ case MATCH_SEARCH:
+ return querySearch(getDocId(uri), getSearchQuery(uri));
+ default:
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed during query", e);
+ return null;
+ }
+ }
+
+ @Override
+ public final String getType(Uri uri) {
+ try {
+ switch (mMatcher.match(uri)) {
+ case MATCH_DOCUMENT:
+ return getType(getDocId(uri));
+ default:
+ return null;
+ }
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed during getType", e);
+ return null;
+ }
+ }
+
+ @Override
+ public final Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException("Insert not supported");
+ }
+
+ @Override
+ public final int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("Delete not supported");
+ }
+
+ @Override
+ public final int update(
+ Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("Update not supported");
+ }
+
+ @Override
+ public final Bundle callFromPackage(
+ String callingPackage, String method, String arg, Bundle extras) {
+ if (!method.startsWith("android:")) {
+ // Let non-platform methods pass through
+ return super.callFromPackage(callingPackage, method, arg, extras);
+ }
+
+ // Platform operations require the caller explicitly hold manage
+ // permission; Uri permissions don't extend management operations.
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.MANAGE_DOCUMENTS, "Document management");
+
+ final Bundle out = new Bundle();
+ try {
+ if (METHOD_GET_ROOTS.equals(method)) {
+ final List<DocumentRoot> roots = getDocumentRoots();
+ out.putParcelableList(EXTRA_ROOTS, roots);
+
+ } else if (METHOD_CREATE_DOCUMENT.equals(method)) {
+ final String docId = extras.getString(DocumentColumns.DOC_ID);
+ final String mimeType = extras.getString(DocumentColumns.MIME_TYPE);
+ final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
+
+ // TODO: issue Uri grant towards caller
+ final String newDocId = createDocument(docId, mimeType, displayName);
+ out.putString(DocumentColumns.DOC_ID, newDocId);
+
+ } else if (METHOD_RENAME_DOCUMENT.equals(method)) {
+ final String docId = extras.getString(DocumentColumns.DOC_ID);
+ final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
+ renameDocument(docId, displayName);
+
+ } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
+ final String docId = extras.getString(DocumentColumns.DOC_ID);
+ deleteDocument(docId);
+
+ } else {
+ throw new UnsupportedOperationException("Method not supported " + method);
+ }
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException("Failed call " + method, e);
+ }
+ return out;
+ }
+
+ @Override
+ public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ return openDocument(getDocId(uri), mode, null);
+ }
+
+ @Override
+ public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
+ throws FileNotFoundException {
+ return openDocument(getDocId(uri), mode, signal);
+ }
+
+ @Override
+ public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
+ throws FileNotFoundException {
+ if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
+ final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
+ return openDocumentThumbnail(getDocId(uri), sizeHint, null);
+ } else {
+ return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
+ }
+ }
+
+ @Override
+ public final AssetFileDescriptor openTypedAssetFile(
+ Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
+ throws FileNotFoundException {
+ if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
+ final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
+ return openDocumentThumbnail(getDocId(uri), sizeHint, signal);
+ } else {
+ return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
+ }
+ }
+
+ /**
+ * Notify system that {@link #getDocumentRoots()} has changed, usually due to an
+ * account or device change.
+ */
+ public void notifyDocumentRootsChanged() {
+ final Intent intent = new Intent(ACTION_DOCUMENT_ROOT_CHANGED);
+ intent.putExtra(EXTRA_AUTHORITY, mAuthority);
+ getContext().sendBroadcast(intent);
+ }
+}
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index d79f5c6..6cc92e3 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -35,9 +35,9 @@
</intent-filter>
<!-- data expected to point at existing root to manage -->
<intent-filter>
- <action android:name="android.intent.action.MANAGE_DOCUMENT" />
+ <action android:name="android.provider.action.MANAGE_DOCUMENTS" />
<category android:name="android.intent.category.DEFAULT" />
- <data android:mimeType="vnd.android.cursor.item/root" />
+ <data android:mimeType="vnd.android.doc/dir" />
</intent-filter>
</activity>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
index 575947f..6bc554f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -20,14 +20,14 @@
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.net.Uri;
import android.os.Bundle;
-import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Documents;
import android.view.LayoutInflater;
import android.view.View;
@@ -36,8 +36,6 @@
import com.android.documentsui.model.Document;
-import java.io.FileNotFoundException;
-
/**
* Dialog to create a new directory.
*/
@@ -58,7 +56,7 @@
final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
- final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
+ final EditText text1 = (EditText) view.findViewById(android.R.id.text1);
builder.setTitle(R.string.menu_create_dir);
builder.setView(view);
@@ -68,24 +66,25 @@
public void onClick(DialogInterface dialog, int which) {
final String displayName = text1.getText().toString();
- final ContentValues values = new ContentValues();
- values.put(DocumentColumns.MIME_TYPE, Documents.MIME_TYPE_DIR);
- values.put(DocumentColumns.DISPLAY_NAME, displayName);
-
final DocumentsActivity activity = (DocumentsActivity) getActivity();
final Document cwd = activity.getCurrentDirectory();
- Uri childUri = resolver.insert(cwd.uri, values);
+ final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+ cwd.uri.getAuthority());
try {
+ final String docId = DocumentsContract.createDocument(client,
+ DocumentsContract.getDocId(cwd.uri), Documents.MIME_TYPE_DIR,
+ displayName);
+
// Navigate into newly created child
+ final Uri childUri = DocumentsContract.buildDocumentUri(
+ cwd.uri.getAuthority(), docId);
final Document childDoc = Document.fromUri(resolver, childUri);
activity.onDocumentPicked(childDoc);
- } catch (FileNotFoundException e) {
- childUri = null;
- }
-
- if (childUri == null) {
+ } catch (Exception e) {
Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
+ } finally {
+ ContentProviderClient.closeQuietly(client);
}
}
});
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index dd9aee5..783b6ff 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -20,8 +20,8 @@
import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DATE;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_NAME;
+import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_SIZE;
import android.app.Fragment;
@@ -32,7 +32,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
-import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
@@ -55,7 +54,6 @@
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
-import android.widget.Button;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListView;
@@ -64,7 +62,6 @@
import com.android.documentsui.DocumentsActivity.DisplayState;
import com.android.documentsui.model.Document;
-import com.android.documentsui.model.Root;
import com.android.internal.util.Predicate;
import com.google.android.collect.Lists;
@@ -81,7 +78,6 @@
private View mEmptyView;
private ListView mListView;
private GridView mGridView;
- private Button mMoreView;
private AbsListView mCurrentView;
@@ -110,7 +106,8 @@
}
public static void showSearch(FragmentManager fm, Uri uri, String query) {
- final Uri searchUri = DocumentsContract.buildSearchUri(uri, query);
+ final Uri searchUri = DocumentsContract.buildSearchUri(
+ uri.getAuthority(), DocumentsContract.getDocId(uri), query);
show(fm, TYPE_SEARCH, searchUri);
}
@@ -153,8 +150,6 @@
mGridView.setOnItemClickListener(mItemListener);
mGridView.setMultiChoiceModeListener(mMultiListener);
- mMoreView = (Button) view.findViewById(R.id.more);
-
mAdapter = new DocumentsAdapter();
final Uri uri = getArguments().getParcelable(EXTRA_URI);
@@ -168,22 +163,19 @@
Uri contentsUri;
if (mType == TYPE_NORMAL) {
- contentsUri = DocumentsContract.buildContentsUri(uri);
+ contentsUri = DocumentsContract.buildChildrenUri(
+ uri.getAuthority(), DocumentsContract.getDocId(uri));
} else if (mType == TYPE_RECENT_OPEN) {
contentsUri = RecentsProvider.buildRecentOpen();
} else {
contentsUri = uri;
}
- if (state.localOnly) {
- contentsUri = DocumentsContract.setLocalOnly(contentsUri);
- }
-
final Comparator<Document> sortOrder;
- if (state.sortOrder == SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
- sortOrder = new Document.DateComparator();
- } else if (state.sortOrder == SORT_ORDER_NAME) {
- sortOrder = new Document.NameComparator();
+ if (state.sortOrder == SORT_ORDER_LAST_MODIFIED || mType == TYPE_RECENT_OPEN) {
+ sortOrder = new Document.LastModifiedComparator();
+ } else if (state.sortOrder == SORT_ORDER_DISPLAY_NAME) {
+ sortOrder = new Document.DisplayNameComparator();
} else if (state.sortOrder == SORT_ORDER_SIZE) {
sortOrder = new Document.SizeComparator();
} else {
@@ -196,28 +188,6 @@
@Override
public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
mAdapter.swapDocuments(result.contents);
-
- final Cursor cursor = result.cursor;
- if (cursor != null && cursor.getExtras()
- .getBoolean(DocumentsContract.EXTRA_HAS_MORE, false)) {
- mMoreView.setText(R.string.more);
- mMoreView.setVisibility(View.VISIBLE);
- mMoreView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- mMoreView.setText(R.string.loading);
- final Bundle bundle = new Bundle();
- bundle.putBoolean(DocumentsContract.EXTRA_REQUEST_MORE, true);
- try {
- cursor.respond(bundle);
- } catch (Exception e) {
- Log.w(TAG, "Failed to respond: " + e);
- }
- }
- });
- } else {
- mMoreView.setVisibility(View.GONE);
- }
}
@Override
@@ -489,8 +459,7 @@
task.execute(doc.uri);
}
} else {
- icon.setImageDrawable(roots.resolveDocumentIcon(
- context, doc.uri.getAuthority(), doc.mimeType));
+ icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, doc.mimeType));
}
title.setText(doc.displayName);
@@ -504,11 +473,7 @@
summary.setVisibility(View.INVISIBLE);
}
} else if (mType == TYPE_RECENT_OPEN) {
- final Root root = roots.findRoot(doc);
- icon1.setVisibility(View.VISIBLE);
- icon1.setImageDrawable(root.icon);
- summary.setText(root.getDirectoryString());
- summary.setVisibility(View.VISIBLE);
+ // TODO: resolve storage root
}
if (summaryGrid != null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index 14d6fd5..4ce5ef8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -26,7 +26,6 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.CancellationSignal;
-import android.provider.DocumentsContract.DocumentColumns;
import android.util.Log;
import com.android.documentsui.model.Document;
@@ -77,9 +76,10 @@
}
private void loadInBackgroundInternal(
- DirectoryResult result, Uri uri, CancellationSignal signal) {
+ DirectoryResult result, Uri uri, CancellationSignal signal) throws RuntimeException {
+ // TODO: switch to using unstable CPC
final ContentResolver resolver = getContext().getContentResolver();
- final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal);
+ final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
result.cursor = cursor;
result.cursor.registerContentObserver(mObserver);
@@ -110,16 +110,4 @@
Collections.sort(result.contents, mSortOrder);
}
}
-
- private String getQuerySortOrder() {
- if (mSortOrder instanceof Document.DateComparator) {
- return DocumentColumns.LAST_MODIFIED + " DESC";
- } else if (mSortOrder instanceof Document.NameComparator) {
- return DocumentColumns.DISPLAY_NAME + " ASC";
- } else if (mSortOrder instanceof Document.SizeComparator) {
- return DocumentColumns.SIZE + " DESC";
- } else {
- return null;
- }
- }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
index 72afd9e..0ce5968 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
@@ -21,12 +21,11 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.provider.DocumentsContract.DocumentRoot;
import android.util.Log;
-import com.android.documentsui.model.Root;
-
/**
- * Handles {@link Root} changes which invalidate cached data.
+ * Handles {@link DocumentRoot} changes which invalidate cached data.
*/
public class DocumentChangedReceiver extends BroadcastReceiver {
@Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 22e3b98..73ca8fa 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -22,7 +22,7 @@
import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_OPEN;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DATE;
+import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
import android.app.ActionBar;
import android.app.ActionBar.OnNavigationListener;
@@ -32,6 +32,7 @@
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
@@ -41,7 +42,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.DocumentRoot;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
@@ -61,7 +62,6 @@
import com.android.documentsui.model.Document;
import com.android.documentsui.model.DocumentStack;
-import com.android.documentsui.model.Root;
import java.io.FileNotFoundException;
import java.util.Arrays;
@@ -101,7 +101,7 @@
mAction = ACTION_CREATE;
} else if (Intent.ACTION_GET_CONTENT.equals(action)) {
mAction = ACTION_GET_CONTENT;
- } else if (Intent.ACTION_MANAGE_DOCUMENT.equals(action)) {
+ } else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) {
mAction = ACTION_MANAGE;
}
@@ -143,7 +143,7 @@
}
if (mAction == ACTION_MANAGE) {
- mDisplayState.sortOrder = SORT_ORDER_DATE;
+ mDisplayState.sortOrder = SORT_ORDER_LAST_MODIFIED;
}
mRootsContainer = findViewById(R.id.container_roots);
@@ -160,10 +160,7 @@
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
final Uri rootUri = intent.getData();
- final String authority = rootUri.getAuthority();
- final String rootId = DocumentsContract.getRootId(rootUri);
-
- final Root root = mRoots.findRoot(authority, rootId);
+ final DocumentRoot root = mRoots.findRoot(rootUri);
if (root != null) {
onRootPicked(root, true);
} else {
@@ -255,10 +252,10 @@
mDrawerToggle.setDrawerIndicatorEnabled(true);
} else {
- final Root root = getCurrentRoot();
- actionBar.setIcon(root != null ? root.icon : null);
+ final DocumentRoot root = getCurrentRoot();
+ actionBar.setIcon(root != null ? root.loadIcon(this) : null);
- if (root.isRecents) {
+ if (mRoots.isRecentsRoot(root)) {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setTitle(root.title);
} else {
@@ -441,9 +438,8 @@
final TextView title = (TextView) convertView.findViewById(android.R.id.title);
final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
- final Document cwd = getCurrentDirectory();
- if (cwd != null) {
- title.setText(cwd.displayName);
+ if (mStack.size() > 0) {
+ title.setText(mStack.getTitle(mRoots));
} else {
// No directory means recents
title.setText(R.string.root_recent);
@@ -477,10 +473,9 @@
}
};
- public Root getCurrentRoot() {
- final Document cwd = getCurrentDirectory();
- if (cwd != null) {
- return mRoots.findRoot(cwd);
+ public DocumentRoot getCurrentRoot() {
+ if (mStack.size() > 0) {
+ return mStack.getRoot(mRoots);
} else {
return mRoots.getRecentsRoot();
}
@@ -538,13 +533,14 @@
onCurrentDirectoryChanged();
}
- public void onRootPicked(Root root, boolean closeDrawer) {
+ public void onRootPicked(DocumentRoot root, boolean closeDrawer) {
// Clear entire backstack and start in new root
mStack.clear();
- if (!root.isRecents) {
+ if (!mRoots.isRecentsRoot(root)) {
try {
- onDocumentPicked(Document.fromRoot(getContentResolver(), root));
+ final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.docId);
+ onDocumentPicked(Document.fromUri(getContentResolver(), uri));
} catch (FileNotFoundException e) {
}
} else {
@@ -611,16 +607,21 @@
}
public void onSaveRequested(String mimeType, String displayName) {
- final ContentValues values = new ContentValues();
- values.put(DocumentColumns.MIME_TYPE, mimeType);
- values.put(DocumentColumns.DISPLAY_NAME, displayName);
-
final Document cwd = getCurrentDirectory();
- final Uri childUri = getContentResolver().insert(cwd.uri, values);
- if (childUri != null) {
+ final String authority = cwd.uri.getAuthority();
+
+ final ContentProviderClient client = getContentResolver()
+ .acquireUnstableContentProviderClient(authority);
+ try {
+ final String docId = DocumentsContract.createDocument(client,
+ DocumentsContract.getDocId(cwd.uri), mimeType, displayName);
+
+ final Uri childUri = DocumentsContract.buildDocumentUri(authority, docId);
onFinished(childUri);
- } else {
+ } catch (Exception e) {
Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
+ } finally {
+ ContentProviderClient.closeQuietly(client);
}
}
@@ -680,7 +681,7 @@
public int action;
public int mode = MODE_LIST;
public String[] acceptMimes;
- public int sortOrder = SORT_ORDER_NAME;
+ public int sortOrder = SORT_ORDER_DISPLAY_NAME;
public boolean allowMultiple = false;
public boolean showSize = false;
public boolean localOnly = false;
@@ -693,8 +694,8 @@
public static final int MODE_LIST = 0;
public static final int MODE_GRID = 1;
- public static final int SORT_ORDER_NAME = 0;
- public static final int SORT_ORDER_DATE = 1;
+ 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;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index 5466dbf..3447a51 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -29,6 +29,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.provider.DocumentsContract.DocumentRoot;
import android.text.TextUtils.TruncateAt;
import android.util.Log;
import android.view.LayoutInflater;
@@ -42,7 +43,6 @@
import android.widget.TextView;
import com.android.documentsui.model.DocumentStack;
-import com.android.documentsui.model.Root;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
@@ -181,8 +181,8 @@
final View summaryList = convertView.findViewById(R.id.summary_list);
final DocumentStack stack = getItem(position);
- final Root root = roots.findRoot(stack.peek());
- icon.setImageDrawable(root != null ? root.icon : null);
+ final DocumentRoot root = stack.getRoot(roots);
+ icon.setImageDrawable(root.loadIcon(context));
final StringBuilder builder = new StringBuilder();
for (int i = stack.size() - 1; i >= 0; i--) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index c3b498e..aa21457 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -18,30 +18,24 @@
import static com.android.documentsui.DocumentsActivity.TAG;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
-import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.DocumentRoot;
import android.provider.DocumentsContract.Documents;
import android.util.Log;
-import android.util.Pair;
-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.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Objects;
import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
/**
@@ -54,14 +48,9 @@
private final Context mContext;
- /** Map from authority to cached info */
- private HashMap<String, DocumentsProviderInfo> mProviders = Maps.newHashMap();
- /** Map from (authority+rootId) to cached info */
- private HashMap<Pair<String, String>, Root> mRoots = Maps.newHashMap();
+ public List<DocumentRoot> mRoots = Lists.newArrayList();
- public ArrayList<Root> mRootsList = Lists.newArrayList();
-
- private Root mRecentsRoot;
+ private DocumentRoot mRecentsRoot;
public RootsCache(Context context) {
mContext = context;
@@ -73,95 +62,78 @@
*/
@GuardedBy("ActivityThread")
public void update() {
- mProviders.clear();
mRoots.clear();
- mRootsList.clear();
{
// Create special root for recents
- final Root root = Root.buildRecents(mContext);
- mRootsList.add(root);
+ final DocumentRoot root = new DocumentRoot();
+ root.rootType = DocumentRoot.ROOT_TYPE_SHORTCUT;
+ root.docId = null;
+ root.icon = R.drawable.ic_dir;
+ root.title = mContext.getString(R.string.root_recent);
+ root.summary = null;
+ root.availableBytes = -1;
+
+ mRoots.add(root);
mRecentsRoot = root;
}
// Query for other storage backends
+ final ContentResolver resolver = mContext.getContentResolver();
final PackageManager pm = mContext.getPackageManager();
final List<ProviderInfo> providers = pm.queryContentProviders(
null, -1, PackageManager.GET_META_DATA);
- for (ProviderInfo providerInfo : providers) {
- if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
+ for (ProviderInfo info : providers) {
+ if (info.metaData != null && info.metaData.containsKey(
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
- final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
- mContext, providerInfo);
- if (info == null) {
- Log.w(TAG, "Missing info for " + providerInfo);
- continue;
- }
- mProviders.put(info.providerInfo.authority, info);
-
+ // TODO: remove deprecated customRoots flag
+ // TODO: populate roots on background thread, and cache results
+ final ContentProviderClient client = resolver
+ .acquireUnstableContentProviderClient(info.authority);
try {
- // TODO: remove deprecated customRoots flag
- // TODO: populate roots on background thread, and cache results
- final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
- final Cursor cursor = mContext.getContentResolver()
- .query(uri, null, null, null, null);
- try {
- while (cursor.moveToNext()) {
- final Root root = Root.fromCursor(mContext, info, cursor);
- mRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
- mRootsList.add(root);
- }
- } finally {
- cursor.close();
+ final List<DocumentRoot> roots = DocumentsContract.getDocumentRoots(client);
+ for (DocumentRoot root : roots) {
+ root.authority = info.authority;
}
+ mRoots.addAll(roots);
} catch (Exception e) {
- Log.w(TAG, "Failed to load some roots from " + info.providerInfo.authority
- + ": " + e);
+ Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e);
+ } finally {
+ ContentProviderClient.closeQuietly(client);
}
}
}
}
- @GuardedBy("ActivityThread")
- public DocumentsProviderInfo findProvider(String authority) {
- return mProviders.get(authority);
+ public DocumentRoot findRoot(Uri uri) {
+ final String authority = uri.getAuthority();
+ final String docId = DocumentsContract.getDocId(uri);
+ for (DocumentRoot root : mRoots) {
+ if (Objects.equal(root.authority, authority) && Objects.equal(root.docId, docId)) {
+ return root;
+ }
+ }
+ return null;
}
@GuardedBy("ActivityThread")
- public Root findRoot(String authority, String rootId) {
- return mRoots.get(Pair.create(authority, rootId));
- }
-
- @GuardedBy("ActivityThread")
- public Root findRoot(Document doc) {
- final String authority = doc.uri.getAuthority();
- final String rootId = DocumentsContract.getRootId(doc.uri);
- return findRoot(authority, rootId);
- }
-
- @GuardedBy("ActivityThread")
- public Root getRecentsRoot() {
+ public DocumentRoot getRecentsRoot() {
return mRecentsRoot;
}
@GuardedBy("ActivityThread")
- public Collection<Root> getRoots() {
- return mRootsList;
+ public boolean isRecentsRoot(DocumentRoot root) {
+ return mRecentsRoot == root;
}
@GuardedBy("ActivityThread")
- public Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
- // Custom icons take precedence
- final DocumentsProviderInfo info = mProviders.get(authority);
- if (info != null) {
- for (Icon icon : info.customIcons) {
- if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
- return icon.icon;
- }
- }
- }
+ public List<DocumentRoot> getRoots() {
+ return mRoots;
+ }
+ @GuardedBy("ActivityThread")
+ public static Drawable resolveDocumentIcon(Context context, String mimeType) {
if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
return context.getResources().getDrawable(R.drawable.ic_dir);
} else {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index 8a48e2a..2cfa841 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -26,7 +26,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
-import android.provider.DocumentsContract.Roots;
+import android.provider.DocumentsContract.DocumentRoot;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
@@ -40,10 +40,9 @@
import android.widget.TextView;
import com.android.documentsui.SectionedListAdapter.SectionAdapter;
-import com.android.documentsui.model.Root;
-import com.android.documentsui.model.Root.RootComparator;
+import com.android.documentsui.model.Document;
-import java.util.Collection;
+import java.util.Comparator;
import java.util.List;
/**
@@ -102,8 +101,8 @@
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
final Object item = mAdapter.getItem(position);
- if (item instanceof Root) {
- activity.onRootPicked((Root) item, true);
+ if (item instanceof DocumentRoot) {
+ activity.onRootPicked((DocumentRoot) item, true);
} else if (item instanceof ResolveInfo) {
activity.onAppPicked((ResolveInfo) item);
} else {
@@ -112,7 +111,7 @@
}
};
- private static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
+ private static class RootsAdapter extends ArrayAdapter<DocumentRoot> implements SectionAdapter {
private int mHeaderId;
public RootsAdapter(Context context, int headerId) {
@@ -132,14 +131,14 @@
final TextView title = (TextView) convertView.findViewById(android.R.id.title);
final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
- final Root root = getItem(position);
- icon.setImageDrawable(root.icon);
+ final DocumentRoot root = getItem(position);
+ icon.setImageDrawable(root.loadIcon(context));
title.setText(root.title);
// Device summary is always available space
final String summaryText;
- if ((root.rootType == Roots.ROOT_TYPE_DEVICE
- || root.rootType == Roots.ROOT_TYPE_DEVICE_ADVANCED)
+ if ((root.rootType == DocumentRoot.ROOT_TYPE_DEVICE
+ || root.rootType == DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED)
&& root.availableBytes >= 0) {
summaryText = context.getString(R.string.root_available_bytes,
Formatter.formatFileSize(context, root.availableBytes));
@@ -216,27 +215,27 @@
private final RootsAdapter mDevicesAdvanced;
private final AppsAdapter mApps;
- public SectionedRootsAdapter(Context context, Collection<Root> roots, Intent includeApps) {
+ public SectionedRootsAdapter(Context context, List<DocumentRoot> roots, Intent includeApps) {
mServices = new RootsAdapter(context, R.string.root_type_service);
mShortcuts = new RootsAdapter(context, R.string.root_type_shortcut);
mDevices = new RootsAdapter(context, R.string.root_type_device);
mDevicesAdvanced = new RootsAdapter(context, R.string.root_type_device);
mApps = new AppsAdapter(context);
- for (Root root : roots) {
+ for (DocumentRoot root : roots) {
Log.d(TAG, "Found rootType=" + root.rootType);
switch (root.rootType) {
- case Roots.ROOT_TYPE_SERVICE:
+ case DocumentRoot.ROOT_TYPE_SERVICE:
mServices.add(root);
break;
- case Roots.ROOT_TYPE_SHORTCUT:
+ case DocumentRoot.ROOT_TYPE_SHORTCUT:
mShortcuts.add(root);
break;
- case Roots.ROOT_TYPE_DEVICE:
+ case DocumentRoot.ROOT_TYPE_DEVICE:
mDevices.add(root);
mDevicesAdvanced.add(root);
break;
- case Roots.ROOT_TYPE_DEVICE_ADVANCED:
+ case DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED:
mDevicesAdvanced.add(root);
break;
}
@@ -281,4 +280,16 @@
}
}
}
+
+ public static class RootComparator implements Comparator<DocumentRoot> {
+ @Override
+ public int compare(DocumentRoot lhs, DocumentRoot rhs) {
+ final int score = Document.compareToIgnoreCaseNullable(lhs.title, rhs.title);
+ if (score != 0) {
+ return score;
+ } else {
+ return Document.compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
+ }
+ }
+ }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index 8eb81b8..7e1a297 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -73,8 +73,8 @@
final View view = inflater.inflate(R.layout.fragment_save, container, false);
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
- icon.setImageDrawable(roots.resolveDocumentIcon(
- context, null, getArguments().getString(EXTRA_MIME_TYPE)));
+ icon.setImageDrawable(
+ RootsCache.resolveDocumentIcon(context, getArguments().getString(EXTRA_MIME_TYPE)));
mDisplayName = (EditText) view.findViewById(android.R.id.title);
mDisplayName.addTextChangedListener(mDisplayNameWatcher);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
index c0f21cb..692d171 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
@@ -53,17 +53,11 @@
this.size = size;
}
- public static Document fromRoot(ContentResolver resolver, Root root)
- throws FileNotFoundException {
- 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 Uri uri = DocumentsContract.buildDocumentUri(authority, 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);
@@ -74,6 +68,7 @@
return new Document(uri, mimeType, displayName, lastModified, flags, summary, size);
}
+ @Deprecated
public static Document fromRecentOpenCursor(ContentResolver resolver, Cursor recentCursor)
throws FileNotFoundException {
final Uri uri = Uri.parse(getCursorString(recentCursor, RecentsProvider.COL_URI));
@@ -176,7 +171,7 @@
return (index != -1) ? cursor.getInt(index) : 0;
}
- public static class NameComparator implements Comparator<Document> {
+ public static class DisplayNameComparator implements Comparator<Document> {
@Override
public int compare(Document lhs, Document rhs) {
final boolean leftDir = lhs.isDirectory();
@@ -185,12 +180,12 @@
if (leftDir != rightDir) {
return leftDir ? -1 : 1;
} else {
- return Root.compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
+ return compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
}
}
}
- public static class DateComparator implements Comparator<Document> {
+ public static class LastModifiedComparator implements Comparator<Document> {
@Override
public int compare(Document lhs, Document rhs) {
return Long.compare(rhs.lastModified, lhs.lastModified);
@@ -213,4 +208,10 @@
fnfe.initCause(t);
throw fnfe;
}
+
+ public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
+ if (lhs == null) return -1;
+ if (rhs == null) return 1;
+ return lhs.compareToIgnoreCase(rhs);
+ }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
index d6c852e..81f75d2 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
@@ -21,8 +21,11 @@
import android.content.ContentResolver;
import android.net.Uri;
+import android.provider.DocumentsContract.DocumentRoot;
import android.util.Log;
+import com.android.documentsui.RootsCache;
+
import org.json.JSONArray;
import org.json.JSONException;
@@ -62,4 +65,18 @@
// TODO: handle roots that have gone missing
return stack;
}
+
+ public DocumentRoot getRoot(RootsCache roots) {
+ return roots.findRoot(getLast().uri);
+ }
+
+ public String getTitle(RootsCache roots) {
+ if (size() == 1) {
+ return getRoot(roots).title;
+ } else if (size() > 1) {
+ return peek().displayName;
+ } else {
+ return null;
+ }
+ }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java
deleted file mode 100644
index 96eb58e..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 23d16df..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.Documents;
-import android.provider.DocumentsContract.RootColumns;
-import android.provider.DocumentsContract.Roots;
-
-import com.android.documentsui.R;
-
-import java.util.Comparator;
-
-/**
- * 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 long availableBytes = -1;
- public boolean isRecents;
-
- public static Root buildRecents(Context context) {
- final PackageManager pm = context.getPackageManager();
- final Root root = new Root();
- root.rootId = null;
- root.rootType = Roots.ROOT_TYPE_SHORTCUT;
- root.uri = null;
- root.icon = context.getResources().getDrawable(R.drawable.ic_dir);
- root.title = context.getString(R.string.root_recent);
- root.summary = null;
- root.availableBytes = -1;
- 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, Documents.DOC_ID_ROOT);
- root.icon = info.providerInfo.loadIcon(pm);
- root.title = info.providerInfo.loadLabel(pm).toString();
- root.availableBytes = cursor.getLong(cursor.getColumnIndex(RootColumns.AVAILABLE_BYTES));
- 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;
- }
-
- /**
- * Return string most suited to showing in a directory listing.
- */
- public String getDirectoryString() {
- return (summary != null) ? summary : title;
- }
-
- public static class RootComparator implements Comparator<Root> {
- @Override
- public int compare(Root lhs, Root rhs) {
- final int score = compareToIgnoreCaseNullable(lhs.title, rhs.title);
- if (score != 0) {
- return score;
- } else {
- return compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
- }
- }
- }
-
- public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
- if (lhs == null) return -1;
- if (rhs == null) return 1;
- return lhs.compareToIgnoreCase(rhs);
- }
-}
diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml
index 8bd2a6d..5272166 100644
--- a/packages/ExternalStorageProvider/AndroidManifest.xml
+++ b/packages/ExternalStorageProvider/AndroidManifest.xml
@@ -15,18 +15,5 @@
android:name="android.content.DOCUMENT_PROVIDER"
android:resource="@xml/document_provider" />
</provider>
-
- <!-- TODO: remove when we have real providers -->
- <provider
- android:name=".CloudTestDocumentsProvider"
- android:authorities="com.android.externalstorage.cloudtest"
- android:grantUriPermissions="true"
- android:exported="true"
- android:enabled="false"
- android:permission="android.permission.MANAGE_DOCUMENTS">
- <meta-data
- android:name="android.content.DOCUMENT_PROVIDER"
- android:resource="@xml/document_provider" />
- </provider>
</application>
</manifest>
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java
deleted file mode 100644
index 119d92e..0000000
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * 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.externalstorage;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MatrixCursor.RowBuilder;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.DocumentColumns;
-import android.provider.DocumentsContract.Documents;
-import android.provider.DocumentsContract.RootColumns;
-import android.provider.DocumentsContract.Roots;
-import android.util.Log;
-
-import com.google.android.collect.Lists;
-
-import libcore.io.IoUtils;
-
-import java.io.FileNotFoundException;
-import java.util.List;
-
-public class CloudTestDocumentsProvider extends ContentProvider {
- private static final String TAG = "CloudTest";
-
- private static final String AUTHORITY = "com.android.externalstorage.cloudtest";
-
- private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
- private static final int URI_ROOTS = 1;
- private static final int URI_ROOTS_ID = 2;
- private static final int URI_DOCS_ID = 3;
- private static final int URI_DOCS_ID_CONTENTS = 4;
- private static final int URI_DOCS_ID_SEARCH = 5;
-
- static {
- sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
- sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
- sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
- sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
- sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
- }
-
- private static final String[] ALL_ROOTS_COLUMNS = new String[] {
- RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
- RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
- };
-
- private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
- DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
- DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
- };
-
- private List<String> mKnownDocs = Lists.newArrayList("meow.png", "kittens.pdf");
-
- private int mPage;
-
- @Override
- public boolean onCreate() {
- return true;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- switch (sMatcher.match(uri)) {
- case URI_ROOTS: {
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_ROOTS_COLUMNS);
- includeDefaultRoot(result);
- return result;
- }
- case URI_ROOTS_ID: {
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_ROOTS_COLUMNS);
- includeDefaultRoot(result);
- return result;
- }
- case URI_DOCS_ID: {
- final String docId = DocumentsContract.getDocId(uri);
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
- includeDoc(result, docId);
- return result;
- }
- case URI_DOCS_ID_CONTENTS: {
- final CloudCursor result = new CloudCursor(
- projection != null ? projection : ALL_DOCUMENTS_COLUMNS, uri);
- for (String docId : mKnownDocs) {
- includeDoc(result, docId);
- }
- if (mPage < 3) {
- result.setHasMore();
- }
- result.setNotificationUri(getContext().getContentResolver(), uri);
- return result;
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- private void includeDefaultRoot(MatrixCursor result) {
- final RowBuilder row = result.newRow();
- row.offer(RootColumns.ROOT_ID, "testroot");
- row.offer(RootColumns.ROOT_TYPE, Roots.ROOT_TYPE_SERVICE);
- row.offer(RootColumns.TITLE, "_TestTitle");
- row.offer(RootColumns.SUMMARY, "_TestSummary");
- }
-
- private void includeDoc(MatrixCursor result, String docId) {
- int flags = 0;
-
- final String mimeType;
- if (Documents.DOC_ID_ROOT.equals(docId)) {
- mimeType = Documents.MIME_TYPE_DIR;
- } else {
- mimeType = "application/octet-stream";
- }
-
- final RowBuilder row = result.newRow();
- row.offer(DocumentColumns.DOC_ID, docId);
- row.offer(DocumentColumns.DISPLAY_NAME, docId);
- row.offer(DocumentColumns.MIME_TYPE, mimeType);
- row.offer(DocumentColumns.LAST_MODIFIED, System.currentTimeMillis());
- row.offer(DocumentColumns.FLAGS, flags);
- }
-
- private class CloudCursor extends MatrixCursor {
- private final Uri mUri;
- private Bundle mExtras = new Bundle();
-
- public CloudCursor(String[] columnNames, Uri uri) {
- super(columnNames);
- mUri = uri;
- }
-
- public void setHasMore() {
- mExtras.putBoolean(DocumentsContract.EXTRA_HAS_MORE, true);
- }
-
- @Override
- public Bundle getExtras() {
- Log.d(TAG, "getExtras() " + mExtras);
- return mExtras;
- }
-
- @Override
- public Bundle respond(Bundle extras) {
- extras.size();
- Log.d(TAG, "respond() " + extras);
- if (extras.getBoolean(DocumentsContract.EXTRA_REQUEST_MORE, false)) {
- new CloudTask().execute(mUri);
- }
- return Bundle.EMPTY;
- }
- }
-
- private class CloudTask extends AsyncTask<Uri, Void, Void> {
- @Override
- protected Void doInBackground(Uri... uris) {
- final Uri uri = uris[0];
-
- SystemClock.sleep(1000);
-
- // Grab some files from the cloud
- for (int i = 0; i < 5; i++) {
- mKnownDocs.add("cloud-page" + mPage + "-file" + i);
- }
- mPage++;
-
- Log.d(TAG, "Loaded more; notifying " + uri);
- getContext().getContentResolver().notifyChange(uri, null, false);
- return null;
- }
- }
-
- private interface TypeQuery {
- final String[] PROJECTION = {
- DocumentColumns.MIME_TYPE };
-
- final int MIME_TYPE = 0;
- }
-
- @Override
- public String getType(Uri uri) {
- switch (sMatcher.match(uri)) {
- case URI_ROOTS: {
- return Roots.MIME_TYPE_DIR;
- }
- case URI_ROOTS_ID: {
- return Roots.MIME_TYPE_ITEM;
- }
- case URI_DOCS_ID: {
- final Cursor cursor = query(uri, TypeQuery.PROJECTION, null, null, null);
- try {
- if (cursor.moveToFirst()) {
- return cursor.getString(TypeQuery.MIME_TYPE);
- } else {
- return null;
- }
- } finally {
- IoUtils.closeQuietly(cursor);
- }
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- @Override
- public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
-}
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 8843e19..583ecc9 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -16,205 +16,130 @@
package com.android.externalstorage;
-import android.content.ContentProvider;
import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.UriMatcher;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
+import android.graphics.Point;
import android.media.ExifInterface;
-import android.net.Uri;
-import android.os.Bundle;
+import android.os.CancellationSignal;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
-import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.DocumentRoot;
import android.provider.DocumentsContract.Documents;
-import android.provider.DocumentsContract.RootColumns;
-import android.provider.DocumentsContract.Roots;
-import android.util.Log;
+import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
+import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
-public class ExternalStorageProvider extends ContentProvider {
+public class ExternalStorageProvider extends DocumentsProvider {
private static final String TAG = "ExternalStorage";
- private static final String AUTHORITY = "com.android.externalstorage.documents";
+ // docId format: root:path/to/file
- // TODO: support multiple storage devices
-
- private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
- private static final int URI_ROOTS = 1;
- private static final int URI_ROOTS_ID = 2;
- private static final int URI_DOCS_ID = 3;
- private static final int URI_DOCS_ID_CONTENTS = 4;
- private static final int URI_DOCS_ID_SEARCH = 5;
-
- static {
- sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
- sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
- sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
- sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
- sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
- }
-
- private HashMap<String, Root> mRoots = Maps.newHashMap();
-
- private static class Root {
- public int rootType;
- public String name;
- public int icon = 0;
- public String title = null;
- public String summary = null;
- public File path;
- }
-
- private static final String[] ALL_ROOTS_COLUMNS = new String[] {
- RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
- RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
- };
-
- private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
+ private static final String[] SUPPORTED_COLUMNS = new String[] {
DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
};
+ private ArrayList<DocumentRoot> mRoots;
+ private HashMap<String, DocumentRoot> mTagToRoot;
+ private HashMap<String, File> mTagToPath;
+
@Override
public boolean onCreate() {
- mRoots.clear();
+ mRoots = Lists.newArrayList();
+ mTagToRoot = Maps.newHashMap();
+ mTagToPath = Maps.newHashMap();
- final Root root = new Root();
- root.rootType = Roots.ROOT_TYPE_DEVICE_ADVANCED;
- root.name = "primary";
- root.title = getContext().getString(R.string.root_internal_storage);
- root.path = Environment.getExternalStorageDirectory();
- mRoots.put(root.name, root);
+ // TODO: support multiple storage devices
+
+ try {
+ final String tag = "primary";
+ final File path = Environment.getExternalStorageDirectory();
+ mTagToPath.put(tag, path);
+
+ final DocumentRoot root = new DocumentRoot();
+ root.docId = getDocIdForFile(path);
+ root.rootType = DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED;
+ root.title = getContext().getString(R.string.root_internal_storage);
+ root.icon = R.drawable.ic_pdf;
+ root.flags = DocumentRoot.FLAG_LOCAL_ONLY;
+ mRoots.add(root);
+ mTagToRoot.put(tag, root);
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
return true;
}
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- switch (sMatcher.match(uri)) {
- case URI_ROOTS: {
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_ROOTS_COLUMNS);
- for (Root root : mRoots.values()) {
- includeRoot(result, root);
- }
- return result;
- }
- case URI_ROOTS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+ private String getDocIdForFile(File file) throws FileNotFoundException {
+ String path = file.getAbsolutePath();
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_ROOTS_COLUMNS);
- includeRoot(result, root);
- return result;
- }
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
- final File file = docIdToFile(root, docId);
- includeFile(result, root, file);
- return result;
- }
- case URI_DOCS_ID_CONTENTS: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
- final File parent = docIdToFile(root, docId);
-
- for (File file : parent.listFiles()) {
- includeFile(result, root, file);
- }
-
- return result;
- }
- case URI_DOCS_ID_SEARCH: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
- final String query = DocumentsContract.getSearchQuery(uri).toLowerCase();
-
- final MatrixCursor result = new MatrixCursor(
- projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
- final File parent = docIdToFile(root, docId);
-
- final LinkedList<File> pending = new LinkedList<File>();
- pending.add(parent);
- while (!pending.isEmpty() && result.getCount() < 20) {
- final File file = pending.removeFirst();
- if (file.isDirectory()) {
- for (File child : file.listFiles()) {
- pending.add(child);
- }
- } else {
- if (file.getName().toLowerCase().contains(query)) {
- includeFile(result, root, file);
- }
- }
- }
-
- return result;
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ // Find the most-specific root path
+ Map.Entry<String, File> mostSpecific = null;
+ for (Map.Entry<String, File> root : mTagToPath.entrySet()) {
+ final String rootPath = root.getValue().getPath();
+ if (path.startsWith(rootPath) && (mostSpecific == null
+ || rootPath.length() > mostSpecific.getValue().getPath().length())) {
+ mostSpecific = root;
}
}
- }
- private String fileToDocId(Root root, File file) {
- String rootPath = root.path.getAbsolutePath();
- final String path = file.getAbsolutePath();
- if (path.equals(rootPath)) {
- return Documents.DOC_ID_ROOT;
+ if (mostSpecific == null) {
+ throw new FileNotFoundException("Failed to find root that contains " + path);
}
- if (!rootPath.endsWith("/")) {
- rootPath += "/";
- }
- if (!path.startsWith(rootPath)) {
- throw new IllegalArgumentException("File " + path + " outside root " + root.path);
+ // Start at first char of path under root
+ final String rootPath = mostSpecific.getValue().getPath();
+ if (rootPath.equals(path)) {
+ path = "";
+ } else if (rootPath.endsWith("/")) {
+ path = path.substring(rootPath.length());
} else {
- return path.substring(rootPath.length());
+ path = path.substring(rootPath.length() + 1);
}
+
+ return mostSpecific.getKey() + ':' + path;
}
- private File docIdToFile(Root root, String docId) {
- if (Documents.DOC_ID_ROOT.equals(docId)) {
- return root.path;
+ private File getFileForDocId(String docId) throws FileNotFoundException {
+ final int splitIndex = docId.indexOf(':', 1);
+ final String tag = docId.substring(0, splitIndex);
+ final String path = docId.substring(splitIndex + 1);
+
+ File target = mTagToPath.get(tag);
+ if (target == null) {
+ throw new FileNotFoundException("No root for " + tag);
+ }
+ target = new File(target, path);
+ if (!target.exists()) {
+ throw new FileNotFoundException("Missing file for " + docId + " at " + target);
+ }
+ return target;
+ }
+
+ private void includeFile(MatrixCursor result, String docId, File file)
+ throws FileNotFoundException {
+ if (docId == null) {
+ docId = getDocIdForFile(file);
} else {
- return new File(root.path, docId);
+ file = getFileForDocId(docId);
}
- }
- private void includeRoot(MatrixCursor result, Root root) {
- final RowBuilder row = result.newRow();
- row.offer(RootColumns.ROOT_ID, root.name);
- row.offer(RootColumns.ROOT_TYPE, root.rootType);
- row.offer(RootColumns.ICON, root.icon);
- row.offer(RootColumns.TITLE, root.title);
- row.offer(RootColumns.SUMMARY, root.summary);
- row.offer(RootColumns.AVAILABLE_BYTES, root.path.getFreeSpace());
- }
-
- private void includeFile(MatrixCursor result, Root root, File file) {
int flags = 0;
if (file.isDirectory()) {
@@ -229,19 +154,12 @@
flags |= Documents.FLAG_SUPPORTS_DELETE;
}
+ final String displayName = file.getName();
final String mimeType = getTypeForFile(file);
if (mimeType.startsWith("image/")) {
flags |= Documents.FLAG_SUPPORTS_THUMBNAIL;
}
- final String docId = fileToDocId(root, file);
- final String displayName;
- if (Documents.DOC_ID_ROOT.equals(docId)) {
- displayName = root.title;
- } else {
- displayName = file.getName();
- }
-
final RowBuilder row = result.newRow();
row.offer(DocumentColumns.DOC_ID, docId);
row.offer(DocumentColumns.DISPLAY_NAME, displayName);
@@ -252,26 +170,129 @@
}
@Override
- public String getType(Uri uri) {
- switch (sMatcher.match(uri)) {
- case URI_ROOTS: {
- return Roots.MIME_TYPE_DIR;
+ public List<DocumentRoot> getDocumentRoots() {
+ // Update free space
+ for (String tag : mTagToRoot.keySet()) {
+ final DocumentRoot root = mTagToRoot.get(tag);
+ final File path = mTagToPath.get(tag);
+ root.availableBytes = path.getFreeSpace();
+ }
+ return mRoots;
+ }
+
+ @Override
+ public String createDocument(String docId, String mimeType, String displayName)
+ throws FileNotFoundException {
+ final File parent = getFileForDocId(docId);
+ displayName = validateDisplayName(mimeType, displayName);
+
+ final File file = new File(parent, displayName);
+ if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
+ if (!file.mkdir()) {
+ throw new IllegalStateException("Failed to mkdir " + file);
}
- case URI_ROOTS_ID: {
- return Roots.MIME_TYPE_ITEM;
+ } else {
+ try {
+ if (!file.createNewFile()) {
+ throw new IllegalStateException("Failed to touch " + file);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to touch " + file + ": " + e);
}
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
- return getTypeForFile(docIdToFile(root, docId));
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
+ }
+ return getDocIdForFile(file);
+ }
+
+ @Override
+ public void renameDocument(String docId, String displayName) throws FileNotFoundException {
+ final File file = getFileForDocId(docId);
+ final File newFile = new File(file.getParentFile(), displayName);
+ if (!file.renameTo(newFile)) {
+ throw new IllegalStateException("Failed to rename " + docId);
+ }
+ // TODO: update any outstanding grants
+ }
+
+ @Override
+ public void deleteDocument(String docId) throws FileNotFoundException {
+ final File file = getFileForDocId(docId);
+ if (!file.delete()) {
+ throw new IllegalStateException("Failed to delete " + file);
}
}
- private String getTypeForFile(File file) {
+ @Override
+ public Cursor queryDocument(String docId) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
+ includeFile(result, docId, null);
+ return result;
+ }
+
+ @Override
+ public Cursor queryDocumentChildren(String docId) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
+ final File parent = getFileForDocId(docId);
+ for (File file : parent.listFiles()) {
+ includeFile(result, null, file);
+ }
+ return result;
+ }
+
+ @Override
+ public Cursor querySearch(String docId, String query) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
+ final File parent = getFileForDocId(docId);
+
+ final LinkedList<File> pending = new LinkedList<File>();
+ pending.add(parent);
+ while (!pending.isEmpty() && result.getCount() < 20) {
+ final File file = pending.removeFirst();
+ if (file.isDirectory()) {
+ for (File child : file.listFiles()) {
+ pending.add(child);
+ }
+ } else {
+ if (file.getName().toLowerCase().contains(query)) {
+ includeFile(result, null, file);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public String getType(String docId) throws FileNotFoundException {
+ final File file = getFileForDocId(docId);
+ return getTypeForFile(file);
+ }
+
+ @Override
+ public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+ throws FileNotFoundException {
+ final File file = getFileForDocId(docId);
+ return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(null, mode));
+ }
+
+ @Override
+ public AssetFileDescriptor openDocumentThumbnail(
+ String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+ final File file = getFileForDocId(docId);
+ final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
+ file, ParcelFileDescriptor.MODE_READ_ONLY);
+
+ try {
+ final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
+ final long[] thumb = exif.getThumbnailRange();
+ if (thumb != null) {
+ return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
+ }
+ } catch (IOException e) {
+ }
+
+ return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
+ }
+
+ private static String getTypeForFile(File file) {
if (file.isDirectory()) {
return Documents.MIME_TYPE_DIR;
} else {
@@ -279,7 +300,7 @@
}
}
- private String getTypeForName(String name) {
+ private static String getTypeForName(String name) {
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
@@ -292,129 +313,7 @@
return "application/octet-stream";
}
- @Override
- public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- switch (sMatcher.match(uri)) {
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final File file = docIdToFile(root, docId);
- return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- @Override
- public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
- throws FileNotFoundException {
- if (opts == null || !opts.containsKey(DocumentsContract.EXTRA_THUMBNAIL_SIZE)) {
- return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
- }
-
- switch (sMatcher.match(uri)) {
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final File file = docIdToFile(root, docId);
- final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
- file, ParcelFileDescriptor.MODE_READ_ONLY);
-
- try {
- final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
- final long[] thumb = exif.getThumbnailRange();
- if (thumb != null) {
- return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
- }
- } catch (IOException e) {
- }
-
- return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- switch (sMatcher.match(uri)) {
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final File parent = docIdToFile(root, docId);
-
- final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
- final String name = validateDisplayName(
- values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType);
-
- final File file = new File(parent, name);
- if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
- if (!file.mkdir()) {
- return null;
- }
-
- } else {
- try {
- if (!file.createNewFile()) {
- return null;
- }
- } catch (IOException e) {
- Log.w(TAG, "Failed to create file", e);
- return null;
- }
- }
-
- final String newDocId = fileToDocId(root, file);
- return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- switch (sMatcher.match(uri)) {
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final File file = docIdToFile(root, docId);
- final File newFile = new File(
- file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
- return file.renameTo(newFile) ? 1 : 0;
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- switch (sMatcher.match(uri)) {
- case URI_DOCS_ID: {
- final Root root = mRoots.get(DocumentsContract.getRootId(uri));
- final String docId = DocumentsContract.getDocId(uri);
-
- final File file = docIdToFile(root, docId);
- return file.delete() ? 1 : 0;
- }
- default: {
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
- }
-
- private String validateDisplayName(String displayName, String mimeType) {
+ private static String validateDisplayName(String mimeType, String displayName) {
if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
return displayName;
} else {