diff options
23 files changed, 1103 insertions, 1319 deletions
diff --git a/api/current.txt b/api/current.txt index d249280..8e2b623 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20781,40 +20781,46 @@ package android.provider { } 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 @@ -20822,25 +20828,32 @@ package android.provider { 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 class ContentProviderClient { 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 @@ public class Intent implements Parcelable, Cloneable { @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 android.util.SparseArray; import java.io.Serializable; import java.util.ArrayList; +import java.util.List; import java.util.Set; /** @@ -545,6 +546,13 @@ public final class Bundle implements Parcelable, Cloneable { 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 @@ package android.provider; 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.database.Cursor; 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 @@ import java.util.List; /** * 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 @@ public final class DocumentsContract { * 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 @@ public final class DocumentsContract { * 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 final class DocumentsContract { 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 @@ public final class DocumentsContract { } /** - * 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. + * Extra boolean flag included in a directory {@link Cursor#getExtras()} + * 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 ContentProvider#openTypedAssetFile(Uri, String, Bundle) + * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, + * boolean) */ - public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; + public static final String EXTRA_LOADING = "loading"; /** - * 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. + * 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_HAS_MORE = "has_more"; + public static final String EXTRA_INFO = "info"; /** - * 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. - * - * @see Cursor#respond(Bundle) - * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, - * boolean) + * 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_REQUEST_MORE = "request_more"; + 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_ROOTS = "roots"; 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 final class DocumentsContract { */ public static String getDocId(Uri documentUri) { final List<String> paths = documentUri.getPathSegments(); - if (paths.size() < 4) { - throw new IllegalArgumentException("Not a document: " + documentUri); - } - if (!PATH_ROOTS.equals(paths.get(0))) { + if (paths.size() < 2) { throw new IllegalArgumentException("Not a document: " + documentUri); } - if (!PATH_DOCS.equals(paths.get(2))) { + if (!PATH_DOCS.equals(paths.get(0))) { 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 @@ public final class DocumentsContract { /** * 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 final class DocumentsContract { 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 @@ public final class DocumentsContract { * 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 @@ public final class DocumentsContract { * 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"; + /** + * Flag indicating that at least one directory under this root supports + * creating content. + * + * @see #flags + */ + public static final int FLAG_SUPPORTS_CREATE = 1; /** - * Storage root type, use for clustering. 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 Roots#ROOT_TYPE_SERVICE - * @see Roots#ROOT_TYPE_DEVICE + * @see #flags */ - public static final String ROOT_TYPE = "root_type"; + public static final int FLAG_LOCAL_ONLY = 1 << 1; + + /** {@hide} */ + public String authority; /** - * 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) + * Root type, use for clustering. + * + * @see #ROOT_TYPE_SERVICE + * @see #ROOT_TYPE_DEVICE */ - public static final String ICON = "icon"; + public int rootType; /** - * 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 + * Flags for this root. + * + * @see #FLAG_LOCAL_ONLY */ - public static final String TITLE = "title"; + public int flags; /** - * Summary for this storage root, or {@code null} to omit. This field is - * read-only to document clients. - * <p> - * Type: STRING + * Icon resource ID for this root. */ - public static final String SUMMARY = "summary"; + public int icon; /** - * 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) + * 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 static final String AVAILABLE_BYTES = "available_bytes"; + 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 @@ public final class DocumentsContract { * {@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 @@ public final class DocumentsContract { } } + /** {@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; + } + } + /** - * Create a new document under a specific parent document with the given - * display name and MIME type. + * Create a new document under the given parent document with MIME type and + * display name. * - * @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 + * @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 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); + 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; + } } /** - * Rename the document at the given URI. Given document must have - * {@link Documents#FLAG_SUPPORTS_RENAME} set. + * Rename the given document. * - * @return if rename was successful. + * @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 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 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; + } } /** - * Notify the system that roots have changed for the given storage provider. - * This signal is used to invalidate internal caches. + * Delete the given document. + * + * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE} + * @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 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.AlertDialog; 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 android.widget.Toast; import com.android.documentsui.model.Document; -import java.io.FileNotFoundException; - /** * Dialog to create a new directory. */ @@ -58,7 +56,7 @@ public class CreateDirectoryFragment extends DialogFragment { 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 class CreateDirectoryFragment extends DialogFragment { 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.TAG; 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.ContentResolver; 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.AbsListView.MultiChoiceModeListener; 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 android.widget.Toast; 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 @@ public class DirectoryFragment extends Fragment { private View mEmptyView; private ListView mListView; private GridView mGridView; - private Button mMoreView; private AbsListView mCurrentView; @@ -110,7 +106,8 @@ public class DirectoryFragment extends Fragment { } 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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { @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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { 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.content.Context; 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 @@ public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> { } 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 @@ public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> { 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 static com.android.documentsui.DocumentsActivity.TAG; 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_MANA 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.app.FragmentManager; 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.graphics.drawable.ColorDrawable; 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 android.widget.Toast; 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 @@ public class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { } 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 @@ public class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { 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 class DocumentsActivity extends Activity { } }; - 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 @@ public class DocumentsActivity extends Activity { 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 class DocumentsActivity extends Activity { } 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 class DocumentsActivity extends Activity { 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 class DocumentsActivity extends Activity { 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.database.Cursor; 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.ListView; 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 @@ public class RecentsCreateFragment extends Fragment { 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 @@ package com.android.documentsui; 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 @@ public class RootsCache { 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 @@ public class RootsCache { */ @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); - } - - @GuardedBy("ActivityThread") - public Root findRoot(String authority, String rootId) { - return mRoots.get(Pair.create(authority, rootId)); + 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(Document doc) { - final String authority = doc.uri.getAuthority(); - final String rootId = DocumentsContract.getRootId(doc.uri); - return findRoot(authority, rootId); + public DocumentRoot getRecentsRoot() { + return mRecentsRoot; } @GuardedBy("ActivityThread") - public Root getRecentsRoot() { - return mRecentsRoot; + public boolean isRecentsRoot(DocumentRoot root) { + return mRecentsRoot == root; } @GuardedBy("ActivityThread") - public Collection<Root> getRoots() { - return mRootsList; + public List<DocumentRoot> getRoots() { + return mRoots; } @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 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.Intent; 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.ListView; 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 class RootsFragment extends Fragment { 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 @@ public class RootsFragment extends Fragment { } }; - 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 @@ public class RootsFragment extends Fragment { 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 @@ public class RootsFragment extends Fragment { 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 class RootsFragment extends Fragment { } } } + + 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 @@ public class SaveFragment extends Fragment { 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 @@ public class Document { 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 @@ public class Document { 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 @@ public class Document { 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 @@ public class Document { 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 @@ public class Document { 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 static com.android.documentsui.model.Document.asFileNotFoundException; 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 @@ public class DocumentStack extends LinkedList<Document> { // 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(); - - 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); + mRoots = Lists.newArrayList(); + mTagToRoot = Maps.newHashMap(); + mTagToPath = Maps.newHashMap(); + + // 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; + // 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; } - 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); - } - } - } + if (mostSpecific == null) { + throw new FileNotFoundException("Failed to find root that contains " + path); + } - return result; - } - default: { - throw new UnsupportedOperationException("Unsupported Uri " + uri); - } + // 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 { + path = path.substring(rootPath.length() + 1); } + + return mostSpecific.getKey() + ':' + path; } - 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; - } + 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); - if (!rootPath.endsWith("/")) { - rootPath += "/"; + File target = mTagToPath.get(tag); + if (target == null) { + throw new FileNotFoundException("No root for " + tag); } - if (!path.startsWith(rootPath)) { - throw new IllegalArgumentException("File " + path + " outside root " + root.path); - } else { - return path.substring(rootPath.length()); + target = new File(target, path); + if (!target.exists()) { + throw new FileNotFoundException("Missing file for " + docId + " at " + target); } + return target; } - private File docIdToFile(Root root, String docId) { - if (Documents.DOC_ID_ROOT.equals(docId)) { - return root.path; + 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 @@ public class ExternalStorageProvider extends ContentProvider { 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,169 +170,150 @@ public class ExternalStorageProvider extends ContentProvider { } @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 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); - } + 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; } - private String getTypeForFile(File file) { - if (file.isDirectory()) { - return Documents.MIME_TYPE_DIR; + @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); + } } else { - return getTypeForName(file.getName()); + try { + if (!file.createNewFile()) { + throw new IllegalStateException("Failed to touch " + file); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to touch " + file + ": " + e); + } } + return getDocIdForFile(file); } - private String getTypeForName(String name) { - final int lastDot = name.lastIndexOf('.'); - if (lastDot >= 0) { - final String extension = name.substring(lastDot + 1); - final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mime != null) { - return mime; - } + @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); } - - return "application/octet-stream"; + // TODO: update any outstanding grants } @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); - } + public void deleteDocument(String docId) throws FileNotFoundException { + final File file = getFileForDocId(docId); + if (!file.delete()) { + throw new IllegalStateException("Failed to delete " + file); } } @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); + 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; + } - 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) { + @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 new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); - } - default: { - throw new UnsupportedOperationException("Unsupported Uri " + uri); } } + return result; } @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; - } - } + public String getType(String docId) throws FileNotFoundException { + final File file = getFileForDocId(docId); + return getTypeForFile(file); + } - final String newDocId = fileToDocId(root, file); - return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId); - } - default: { - throw new UnsupportedOperationException("Unsupported Uri " + uri); - } - } + @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 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); + 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); } - @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 static String getTypeForFile(File file) { + if (file.isDirectory()) { + return Documents.MIME_TYPE_DIR; + } else { + return getTypeForName(file.getName()); + } + } + + private static String getTypeForName(String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; } } + + return "application/octet-stream"; } - 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 { |