diff options
28 files changed, 2553 insertions, 1215 deletions
diff --git a/api/current.txt b/api/current.txt index 7aea255564fb..7fbc484a4b4b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20815,67 +20815,70 @@ package android.provider { } public final class DocumentsContract { + method public static android.net.Uri buildChildDocumentsUri(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 buildRecentDocumentsUri(java.lang.String, java.lang.String); + method public static android.net.Uri buildRootsUri(java.lang.String); + method public static android.net.Uri buildSearchDocumentsUri(java.lang.String, java.lang.String, 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 boolean deleteDocument(android.content.ContentResolver, android.net.Uri); + method public static java.lang.String getDocumentId(android.net.Uri); + method public static android.graphics.Bitmap getDocumentThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point, android.os.CancellationSignal); 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 getSearchDocumentsQuery(android.net.Uri); 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 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; + public static final class DocumentsContract.Document { + field public static final java.lang.String COLUMN_DISPLAY_NAME = "_display_name"; + field public static final java.lang.String COLUMN_DOCUMENT_ID = "document_id"; + field public static final java.lang.String COLUMN_FLAGS = "flags"; + field public static final java.lang.String COLUMN_ICON = "icon"; + field public static final java.lang.String COLUMN_LAST_MODIFIED = "last_modified"; + field public static final java.lang.String COLUMN_MIME_TYPE = "mime_type"; + field public static final java.lang.String COLUMN_SIZE = "_size"; + field public static final java.lang.String COLUMN_SUMMARY = "summary"; + field public static final int FLAG_DIR_PREFERS_GRID = 32; // 0x20 + field public static final int FLAG_DIR_SUPPORTS_CREATE = 8; // 0x8 + field public static final int FLAG_DIR_SUPPORTS_SEARCH = 16; // 0x10 + field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4 + field public static final int FLAG_SUPPORTS_THUMBNAIL = 1; // 0x1 + field public static final int FLAG_SUPPORTS_WRITE = 2; // 0x2 + field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.document/directory"; + } + + public static final class DocumentsContract.Root { + field public static final java.lang.String COLUMN_AVAILABLE_BYTES = "available_bytes"; + field public static final java.lang.String COLUMN_DOCUMENT_ID = "document_id"; + field public static final java.lang.String COLUMN_FLAGS = "flags"; + field public static final java.lang.String COLUMN_ICON = "icon"; + field public static final java.lang.String COLUMN_ROOT_ID = "root_id"; + field public static final java.lang.String COLUMN_ROOT_TYPE = "root_type"; + field public static final java.lang.String COLUMN_SUMMARY = "summary"; + field public static final java.lang.String COLUMN_TITLE = "title"; + field public static final int FLAG_ADVANCED = 4; // 0x4 field public static final int FLAG_LOCAL_ONLY = 2; // 0x2 + field public static final int FLAG_PROVIDES_AUDIO = 8; // 0x8 + field public static final int FLAG_PROVIDES_IMAGES = 32; // 0x20 + field public static final int FLAG_PROVIDES_VIDEO = 16; // 0x10 field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1 + field public static final int FLAG_SUPPORTS_RECENTS = 64; // 0x40 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 - field public static final int FLAG_SUPPORTS_RENAME = 2; // 0x2 - 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.doc/dir"; } 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 java.lang.String getDocumentType(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; @@ -20883,10 +20886,11 @@ package android.provider { 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 abstract android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], java.lang.String) throws java.io.FileNotFoundException; + method public abstract android.database.Cursor queryDocument(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; + method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; + method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException; + method public android.database.Cursor querySearchDocuments(java.lang.String, 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[]); } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index ebb7eb861e85..f445fd5481a4 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -19,7 +19,6 @@ package android.provider; import static android.net.TrafficStats.KB_IN_BYTES; import static libcore.io.OsConstants.SEEK_SET; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -30,16 +29,13 @@ 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.CancellationSignal; 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; @@ -62,9 +58,12 @@ import java.util.List; public final class DocumentsContract { private static final String TAG = "Documents"; - // content://com.example/docs/12/ - // content://com.example/docs/12/children/ - // content://com.example/docs/12/search/?query=pony + // content://com.example/root/ + // content://com.example/root/sdcard/ + // content://com.example/root/sdcard/recent/ + // content://com.example/document/12/ + // content://com.example/document/12/children/ + // content://com.example/document/12/search/?query=pony private DocumentsContract() { } @@ -75,279 +74,297 @@ public final class DocumentsContract { /** {@hide} */ 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. + * Constants related to a document, including {@link Cursor} columns names + * and flags. + * <p> + * A document can be either an openable file (with a specific MIME type), or + * a directory containing additional documents (with the + * {@link #MIME_TYPE_DIR} MIME type). + * <p> + * All columns are <em>read-only</em> to client applications. */ - public final static class Documents { - private Documents() { + public final static class Document { + private Document() { } /** - * MIME type of a document which is a directory that may contain additional - * documents. + * Unique ID of a document. This ID is both provided by and interpreted + * by a {@link DocumentsProvider}, and should be treated as an opaque + * value by client applications. + * <p> + * Each document must have a unique ID within a provider, but that + * single document may be included as a child of multiple directories. + * <p> + * A provider must always return durable IDs, since they will be used to + * issue long-term Uri permission grants when an application interacts + * with {@link Intent#ACTION_OPEN_DOCUMENT} and + * {@link Intent#ACTION_CREATE_DOCUMENT}. + * <p> + * Type: STRING */ - public static final String MIME_TYPE_DIR = "vnd.android.doc/dir"; + public static final String COLUMN_DOCUMENT_ID = "document_id"; /** - * Flag indicating that a document is a directory that supports creation of - * new files within it. + * Concrete MIME type of a document. For example, "image/png" or + * "application/pdf" for openable files. A document can also be a + * directory containing additional documents, which is represented with + * the {@link #MIME_TYPE_DIR} MIME type. + * <p> + * Type: STRING * - * @see DocumentColumns#FLAGS + * @see #MIME_TYPE_DIR */ - public static final int FLAG_SUPPORTS_CREATE = 1; + public static final String COLUMN_MIME_TYPE = "mime_type"; /** - * Flag indicating that a document is renamable. + * Display name of a document, used as the primary title displayed to a + * user. + * <p> + * Type: STRING + */ + public static final String COLUMN_DISPLAY_NAME = OpenableColumns.DISPLAY_NAME; + + /** + * Summary of a document, which may be shown to a user. The summary may + * be {@code null}. + * <p> + * Type: STRING + */ + public static final String COLUMN_SUMMARY = "summary"; + + /** + * Timestamp when a document was last modified, in milliseconds since + * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. A + * {@link DocumentsProvider} can update this field using events from + * {@link OnCloseListener} or other reliable + * {@link ParcelFileDescriptor} transports. + * <p> + * Type: INTEGER (long) * - * @see DocumentColumns#FLAGS + * @see System#currentTimeMillis() */ - public static final int FLAG_SUPPORTS_RENAME = 1 << 1; + public static final String COLUMN_LAST_MODIFIED = "last_modified"; /** - * Flag indicating that a document is deletable. + * Specific icon resource ID for a document, or {@code null} to use + * platform default icon based on {@link #COLUMN_MIME_TYPE}. + * <p> + * Type: INTEGER (int) + */ + public static final String COLUMN_ICON = "icon"; + + /** + * Flags that apply to a document. + * <p> + * Type: INTEGER (int) * - * @see DocumentColumns#FLAGS + * @see #FLAG_SUPPORTS_WRITE + * @see #FLAG_SUPPORTS_DELETE + * @see #FLAG_SUPPORTS_THUMBNAIL + * @see #FLAG_DIR_PREFERS_GRID + * @see #FLAG_DIR_SUPPORTS_CREATE + * @see #FLAG_DIR_SUPPORTS_SEARCH */ - public static final int FLAG_SUPPORTS_DELETE = 1 << 2; + public static final String COLUMN_FLAGS = "flags"; /** - * Flag indicating that a document can be represented as a thumbnail. + * Size of a document, in bytes, or {@code null} if unknown. + * <p> + * Type: INTEGER (long) + */ + public static final String COLUMN_SIZE = OpenableColumns.SIZE; + + /** + * MIME type of a document which is a directory that may contain + * additional documents. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_MIME_TYPE */ - public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3; + public static final String MIME_TYPE_DIR = "vnd.android.document/directory"; /** - * Flag indicating that a document is a directory that supports search. + * Flag indicating that a document can be represented as a thumbnail. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_FLAGS + * @see DocumentsContract#getDocumentThumbnail(ContentResolver, Uri, + * Point, CancellationSignal) + * @see DocumentsProvider#openDocumentThumbnail(String, Point, + * android.os.CancellationSignal) */ - public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; + public static final int FLAG_SUPPORTS_THUMBNAIL = 1; /** * Flag indicating that a document supports writing. + * <p> + * When a document is opened with {@link Intent#ACTION_OPEN_DOCUMENT}, + * the calling application is granted both + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. However, the actual + * writability of a document may change over time, for example due to + * remote access changes. This flag indicates that a document client can + * expect {@link ContentResolver#openOutputStream(Uri)} to succeed. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_FLAGS */ - public static final int FLAG_SUPPORTS_WRITE = 1 << 5; + public static final int FLAG_SUPPORTS_WRITE = 1 << 1; /** - * Flag indicating that a document is a directory that prefers its contents - * be shown in a larger format grid. Usually suitable when a directory - * contains mostly pictures. + * Flag indicating that a document is deletable. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_FLAGS + * @see DocumentsContract#deleteDocument(ContentResolver, Uri) + * @see DocumentsProvider#deleteDocument(String) */ - public static final int FLAG_PREFERS_GRID = 1 << 6; - } - - /** - * 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 ContentResolver#notifyChange(Uri, android.database.ContentObserver, - * boolean) - */ - public static final String EXTRA_LOADING = "loading"; - - /** - * Extra string included in a directory {@link Cursor#getExtras()} - * providing an informational message that should be shown to a user. For - * example, a provider may wish to indicate that not all documents are - * available. - */ - public static final String EXTRA_INFO = "info"; - - /** - * Extra string included in a directory {@link Cursor#getExtras()} providing - * an error message that should be shown to a user. For example, a provider - * may wish to indicate that a network error occurred. The user may choose - * to retry, resulting in a new query. - */ - public static final String EXTRA_ERROR = "error"; - - /** {@hide} */ - public static final String METHOD_GET_ROOTS = "android:getRoots"; - /** {@hide} */ - public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; - /** {@hide} */ - public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument"; - /** {@hide} */ - public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; - - /** {@hide} */ - public static final String EXTRA_AUTHORITY = "authority"; - /** {@hide} */ - public static final String EXTRA_PACKAGE_NAME = "packageName"; - /** {@hide} */ - public static final String EXTRA_URI = "uri"; - /** {@hide} */ - public static final String EXTRA_ROOTS = "roots"; - /** {@hide} */ - public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; - - private static final String PATH_DOCS = "docs"; - private static final String PATH_CHILDREN = "children"; - private static final String PATH_SEARCH = "search"; - - private static final String PARAM_QUERY = "query"; + public static final int FLAG_SUPPORTS_DELETE = 1 << 2; - /** - * Build Uri representing the given {@link DocumentColumns#DOC_ID} in a - * document provider. - */ - public static Uri buildDocumentUri(String authority, String docId) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(authority).appendPath(PATH_DOCS).appendPath(docId).build(); - } + /** + * Flag indicating that a document is a directory that supports creation + * of new files within it. Only valid when {@link #COLUMN_MIME_TYPE} is + * {@link #MIME_TYPE_DIR}. + * + * @see #COLUMN_FLAGS + * @see DocumentsContract#createDocument(ContentResolver, Uri, String, + * String) + * @see DocumentsProvider#createDocument(String, String, String) + */ + public static final int FLAG_DIR_SUPPORTS_CREATE = 1 << 3; - /** - * 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 buildChildrenUri(String authority, String docId) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) - .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_CHILDREN).build(); - } + /** + * Flag indicating that a directory supports search. Only valid when + * {@link #COLUMN_MIME_TYPE} is {@link #MIME_TYPE_DIR}. + * + * @see #COLUMN_FLAGS + * @see DocumentsProvider#querySearchDocuments(String, String, + * String[]) + */ + public static final int FLAG_DIR_SUPPORTS_SEARCH = 1 << 4; - /** - * 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 docId, String query) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) - .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_SEARCH) - .appendQueryParameter(PARAM_QUERY, query).build(); + /** + * Flag indicating that a directory prefers its contents be shown in a + * larger format grid. Usually suitable when a directory contains mostly + * pictures. Only valid when {@link #COLUMN_MIME_TYPE} is + * {@link #MIME_TYPE_DIR}. + * + * @see #COLUMN_FLAGS + */ + public static final int FLAG_DIR_PREFERS_GRID = 1 << 5; } /** - * Extract the {@link DocumentColumns#DOC_ID} from the given Uri. + * Constants related to a root of documents, including {@link Cursor} + * columns names and flags. + * <p> + * All columns are <em>read-only</em> to client applications. */ - public static String getDocId(Uri documentUri) { - final List<String> paths = documentUri.getPathSegments(); - if (paths.size() < 2) { - throw new IllegalArgumentException("Not a document: " + documentUri); + public final static class Root { + private Root() { } - if (!PATH_DOCS.equals(paths.get(0))) { - throw new IllegalArgumentException("Not a document: " + documentUri); - } - return paths.get(1); - } - - /** {@hide} */ - public static String getSearchQuery(Uri documentUri) { - return documentUri.getQueryParameter(PARAM_QUERY); - } - /** - * Standard columns for document queries. Document providers <em>must</em> - * support at least these columns when queried. - */ - public interface DocumentColumns extends OpenableColumns { /** - * Unique ID for a document. Values <em>must</em> never change once - * returned, since they may used for long-term Uri permission grants. + * Unique ID of a root. This ID is both provided by and interpreted by a + * {@link DocumentsProvider}, and should be treated as an opaque value + * by client applications. * <p> * Type: STRING */ - public static final String DOC_ID = "doc_id"; + public static final String COLUMN_ROOT_ID = "root_id"; /** - * MIME type of a document. + * Type of a root, used for clustering when presenting multiple roots to + * a user. * <p> - * Type: STRING + * Type: INTEGER (int) * - * @see Documents#MIME_TYPE_DIR + * @see #ROOT_TYPE_SERVICE + * @see #ROOT_TYPE_SHORTCUT + * @see #ROOT_TYPE_DEVICE */ - public static final String MIME_TYPE = "mime_type"; + public static final String COLUMN_ROOT_TYPE = "root_type"; /** - * Timestamp when a document was last modified, in milliseconds since - * 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} transports. + * Flags that apply to a root. * <p> - * Type: INTEGER (long) + * Type: INTEGER (int) * - * @see System#currentTimeMillis() + * @see #FLAG_LOCAL_ONLY + * @see #FLAG_SUPPORTS_CREATE + * @see #FLAG_ADVANCED + * @see #FLAG_PROVIDES_AUDIO + * @see #FLAG_PROVIDES_IMAGES + * @see #FLAG_PROVIDES_VIDEO */ - public static final String LAST_MODIFIED = "last_modified"; + public static final String COLUMN_FLAGS = "flags"; /** - * Specific icon resource for a document, or {@code null} to resolve - * default using {@link #MIME_TYPE}. + * Icon resource ID for a root. * <p> * Type: INTEGER (int) */ - public static final String ICON = "icon"; + public static final String COLUMN_ICON = "icon"; /** - * Summary for a document, or {@code null} to omit. + * Title for a root, which will be shown to a user. * <p> * Type: STRING */ - public static final String SUMMARY = "summary"; + public static final String COLUMN_TITLE = "title"; /** - * Flags that apply to a specific document. + * Summary for this root, which may be shown to a user. The summary may + * be {@code null}. * <p> - * Type: INTEGER (int) + * Type: STRING */ - public static final String FLAGS = "flags"; - } + public static final String COLUMN_SUMMARY = "summary"; - /** - * Metadata about a specific root of documents. - */ - public final static class DocumentRoot implements Parcelable { /** - * Root that represents a storage service, such as a cloud-based - * service. + * Document which is a directory that represents the top directory of + * this root. + * <p> + * Type: STRING * - * @see #rootType + * @see Document#COLUMN_DOCUMENT_ID */ - public static final int ROOT_TYPE_SERVICE = 1; + public static final String COLUMN_DOCUMENT_ID = "document_id"; + + /** + * Number of bytes available in this root, or {@code null} if unknown or + * unbounded. + * <p> + * Type: INTEGER (long) + */ + public static final String COLUMN_AVAILABLE_BYTES = "available_bytes"; /** - * Root that represents a shortcut to content that may be available - * elsewhere through another storage root. + * Type of root that represents a storage service, such as a cloud-based + * service. * - * @see #rootType + * @see #COLUMN_ROOT_TYPE */ - public static final int ROOT_TYPE_SHORTCUT = 2; + public static final int ROOT_TYPE_SERVICE = 1; /** - * Root that represents a physical storage device. + * Type of root that represents a shortcut to content that may be + * available elsewhere through another storage root. * - * @see #rootType + * @see #COLUMN_ROOT_TYPE */ - public static final int ROOT_TYPE_DEVICE = 3; + public static final int ROOT_TYPE_SHORTCUT = 2; /** - * Root that represents a physical storage device that should only be - * displayed to advanced users. + * Type of root that represents a physical storage device. * - * @see #rootType + * @see #COLUMN_ROOT_TYPE */ - public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; + public static final int ROOT_TYPE_DEVICE = 3; /** * Flag indicating that at least one directory under this root supports - * creating content. + * creating content. Roots with this flag will be shown when an + * application interacts with {@link Intent#ACTION_CREATE_DOCUMENT}. * - * @see #flags + * @see #COLUMN_FLAGS */ public static final int FLAG_SUPPORTS_CREATE = 1; @@ -355,138 +372,210 @@ public final class DocumentsContract { * Flag indicating that this root offers content that is strictly local * on the device. That is, no network requests are made for the content. * - * @see #flags + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_LOCAL_ONLY */ public static final int FLAG_LOCAL_ONLY = 1 << 1; - /** {@hide} */ - public String authority; - /** - * Root type, use for clustering. + * Flag indicating that this root should only be visible to advanced + * users. * - * @see #ROOT_TYPE_SERVICE - * @see #ROOT_TYPE_DEVICE + * @see #COLUMN_FLAGS */ - public int rootType; + public static final int FLAG_ADVANCED = 1 << 2; /** - * Flags for this root. + * Flag indicating that a root offers audio documents. When a user is + * selecting audio, roots not providing audio may be excluded. * - * @see #FLAG_LOCAL_ONLY + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_MIME_TYPES */ - public int flags; + public static final int FLAG_PROVIDES_AUDIO = 1 << 3; /** - * Icon resource ID for this root. - */ - public int icon; - - /** - * Title for this root. - */ - public String title; - - /** - * Summary for this root. May be {@code null}. + * Flag indicating that a root offers video documents. When a user is + * selecting video, roots not providing video may be excluded. + * + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_MIME_TYPES */ - public String summary; + public static final int FLAG_PROVIDES_VIDEO = 1 << 4; /** - * Document which is a directory that represents the top of this root. - * Must not be {@code null}. + * Flag indicating that a root offers image documents. When a user is + * selecting images, roots not providing images may be excluded. * - * @see DocumentColumns#DOC_ID + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_MIME_TYPES */ - public String docId; + public static final int FLAG_PROVIDES_IMAGES = 1 << 5; /** - * 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. + * Flag indicating that this root can report recently modified + * documents. * - * @see DocumentColumns#DOC_ID + * @see #COLUMN_FLAGS + * @see DocumentsContract#buildRecentDocumentsUri(String, String) */ - public String recentDocId; + public static final int FLAG_SUPPORTS_RECENTS = 1 << 6; + } - /** - * Number of free bytes of available in this root, or -1 if unknown or - * unbounded. - */ - public long availableBytes; + /** + * Optional 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. The provider must send a content changed + * notification when loading is finished. + * + * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, + * boolean) + */ + public static final String EXTRA_LOADING = "loading"; - /** - * 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; + /** + * Optional string included in a directory {@link Cursor#getExtras()} + * providing an informational message that should be shown to a user. For + * example, a provider may wish to indicate that not all documents are + * available. + */ + public static final String EXTRA_INFO = "info"; - public DocumentRoot() { - } + /** + * Optional string included in a directory {@link Cursor#getExtras()} + * providing an error message that should be shown to a user. For example, a + * provider may wish to indicate that a network error occurred. The user may + * choose to retry, resulting in a new query. + */ + public static final String EXTRA_ERROR = "error"; - /** {@hide} */ - public 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 static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; + /** {@hide} */ + public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; - /** {@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; - } + /** {@hide} */ + public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; - @Override - public int describeContents() { - return 0; - } + private static final String PATH_ROOT = "root"; + private static final String PATH_RECENT = "recent"; + private static final String PATH_DOCUMENT = "document"; + private static final String PATH_CHILDREN = "children"; + private static final String PATH_SEARCH = "search"; - @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); + private static final String PARAM_QUERY = "query"; + + /** + * Build Uri representing the roots of a document provider. When queried, a + * provider will return one or more rows with columns defined by + * {@link Root}. + * + * @see DocumentsProvider#queryRoots(String[]) + */ + public static Uri buildRootsUri(String authority) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(PATH_ROOT).build(); + } + + /** + * Build Uri representing the recently modified documents of a specific + * root. When queried, a provider will return zero or more rows with columns + * defined by {@link Document}. + * + * @see DocumentsProvider#queryRecentDocuments(String, String[]) + * @see #getRootId(Uri) + */ + public static Uri buildRecentDocumentsUri(String authority, String rootId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(PATH_ROOT).appendPath(rootId) + .appendPath(PATH_RECENT).build(); + } + + /** + * Build Uri representing the given {@link Document#COLUMN_DOCUMENT_ID} in a + * document provider. When queried, a provider will return a single row with + * columns defined by {@link Document}. + * + * @see DocumentsProvider#queryDocument(String, String[]) + * @see #getDocumentId(Uri) + */ + public static Uri buildDocumentUri(String authority, String documentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(PATH_DOCUMENT).appendPath(documentId).build(); + } + + /** + * Build Uri representing the children of the given directory in a document + * provider. When queried, a provider will return zero or more rows with + * columns defined by {@link Document}. + * + * @param parentDocumentId the document to return children for, which must + * be a directory with MIME type of + * {@link Document#MIME_TYPE_DIR}. + * @see DocumentsProvider#queryChildDocuments(String, String[], String) + * @see #getDocumentId(Uri) + */ + public static Uri buildChildDocumentsUri(String authority, String parentDocumentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) + .appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_CHILDREN) + .build(); + } + + /** + * Build Uri representing a search for matching documents under a specific + * directory in a document provider. When queried, a provider will return + * zero or more rows with columns defined by {@link Document}. + * + * @param parentDocumentId the document to return children for, which must + * be both a directory with MIME type of + * {@link Document#MIME_TYPE_DIR} and have + * {@link Document#FLAG_DIR_SUPPORTS_SEARCH} set. + * @see DocumentsProvider#querySearchDocuments(String, String, String[]) + * @see #getDocumentId(Uri) + * @see #getSearchDocumentsQuery(Uri) + */ + public static Uri buildSearchDocumentsUri( + String authority, String parentDocumentId, String query) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) + .appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_SEARCH) + .appendQueryParameter(PARAM_QUERY, query).build(); + } + + /** + * Extract the {@link Root#COLUMN_ROOT_ID} from the given Uri. + */ + public static String getRootId(Uri rootUri) { + final List<String> paths = rootUri.getPathSegments(); + if (paths.size() < 2) { + throw new IllegalArgumentException("Not a root: " + rootUri); + } + if (!PATH_ROOT.equals(paths.get(0))) { + throw new IllegalArgumentException("Not a root: " + rootUri); } + return paths.get(1); + } - public static final Creator<DocumentRoot> CREATOR = new Creator<DocumentRoot>() { - @Override - public DocumentRoot createFromParcel(Parcel in) { - return new DocumentRoot(in); - } + /** + * Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given Uri. + */ + public static String getDocumentId(Uri documentUri) { + final List<String> paths = documentUri.getPathSegments(); + if (paths.size() < 2) { + throw new IllegalArgumentException("Not a document: " + documentUri); + } + if (!PATH_DOCUMENT.equals(paths.get(0))) { + throw new IllegalArgumentException("Not a document: " + documentUri); + } + return paths.get(1); + } - @Override - public DocumentRoot[] newArray(int size) { - return new DocumentRoot[size]; - } - }; + /** + * Extract the search query from a Uri built by + * {@link #buildSearchDocumentsUri(String, String, String)}. + */ + public static String getSearchDocumentsQuery(Uri searchDocumentsUri) { + return searchDocumentsUri.getQueryParameter(PARAM_QUERY); } /** @@ -497,6 +586,7 @@ public final class DocumentsContract { * {@link Intent#ACTION_CREATE_DOCUMENT}. * * @see Context#grantUriPermission(String, Uri, int) + * @see Context#revokeUriPermission(Uri, int) * @see ContentResolver#getIncomingUriPermissionGrants(int, int) */ public static Uri[] getOpenDocuments(Context context) { @@ -520,20 +610,28 @@ public final class DocumentsContract { } /** - * Return thumbnail representing the document at the given URI. Callers are - * responsible for their own in-memory caching. Given document must have - * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set. + * Return thumbnail representing the document at the given Uri. Callers are + * responsible for their own in-memory caching. * + * @param documentUri document to return thumbnail for, which must have + * {@link Document#FLAG_SUPPORTS_THUMBNAIL} set. + * @param size optimal thumbnail size desired. A provider may return a + * thumbnail of a different size, but never more than double the + * requested size. + * @param signal signal used to indicate that caller is no longer interested + * in the thumbnail. * @return decoded thumbnail, or {@code null} if problem was encountered. - * @hide + * @see DocumentsProvider#openDocumentThumbnail(String, Point, + * android.os.CancellationSignal) */ - public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) { + public static Bitmap getDocumentThumbnail( + ContentResolver resolver, Uri documentUri, Point size, CancellationSignal signal) { final Bundle openOpts = new Bundle(); openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size); AssetFileDescriptor afd = null; try { - afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts); + afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts, signal); final FileDescriptor fd = afd.getFileDescriptor(); final long offset = afd.getStartOffset(); @@ -583,38 +681,26 @@ 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 the given parent document with MIME type and - * display name. + * Create a new document with given MIME type and display name. * - * @param docId document with {@link Documents#FLAG_SUPPORTS_CREATE} + * @param parentDocumentUri directory with + * {@link Document#FLAG_DIR_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 String createDocument( - ContentProviderClient client, String docId, String mimeType, String displayName) { + public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri, + 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); + in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri)); + in.putString(Document.COLUMN_MIME_TYPE, mimeType); + in.putString(Document.COLUMN_DISPLAY_NAME, displayName); try { - final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in); - return out.getString(DocumentColumns.DOC_ID); + final Bundle out = resolver.call(parentDocumentUri, METHOD_CREATE_DOCUMENT, null, in); + return buildDocumentUri( + parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID)); } catch (Exception e) { Log.w(TAG, "Failed to create document", e); return null; @@ -622,40 +708,16 @@ public final class DocumentsContract { } /** - * Rename the given document. - * - * @param docId document with {@link Documents#FLAG_SUPPORTS_RENAME} - * @return document which may have changed due to rename, or {@code null} if - * rename failed. - * @hide - */ - public static String renameDocument( - ContentProviderClient client, String docId, String displayName) { - final Bundle in = new Bundle(); - in.putString(DocumentColumns.DOC_ID, docId); - in.putString(DocumentColumns.DISPLAY_NAME, displayName); - - try { - final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in); - return out.getString(DocumentColumns.DOC_ID); - } catch (Exception e) { - Log.w(TAG, "Failed to rename document", e); - return null; - } - } - - /** * Delete the given document. * - * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE} - * @hide + * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE} */ - public static boolean deleteDocument(ContentProviderClient client, String docId) { + public static boolean deleteDocument(ContentResolver resolver, Uri documentUri) { final Bundle in = new Bundle(); - in.putString(DocumentColumns.DOC_ID, docId); + in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri)); try { - client.call(METHOD_DELETE_DOCUMENT, null, in); + final Bundle out = resolver.call(documentUri, METHOD_DELETE_DOCUMENT, null, in); return true; } catch (Exception e) { Log.w(TAG, "Failed to delete document", e); diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index eeb8c41435ac..09f4866dda2c 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -16,16 +16,12 @@ 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 static android.provider.DocumentsContract.getDocumentId; +import static android.provider.DocumentsContract.getRootId; +import static android.provider.DocumentsContract.getSearchDocumentsQuery; import android.content.ContentProvider; import android.content.ContentValues; @@ -41,15 +37,12 @@ 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.provider.DocumentsContract.Document; 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 @@ -58,13 +51,13 @@ import java.util.List; * 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. + * {@link Document#COLUMN_DOCUMENT_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. + * {@link Document#MIME_TYPE_DIR} MIME type). Each document can have different + * capabilities, as described by {@link Document#COLUMN_FLAGS}. The same + * {@link Document#COLUMN_DOCUMENT_ID} can be included in multiple directories. * <p> * Document providers must be protected with the * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can @@ -78,22 +71,29 @@ import java.util.List; 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 static final int MATCH_ROOT = 1; + private static final int MATCH_RECENT = 2; + private static final int MATCH_DOCUMENT = 3; + private static final int MATCH_CHILDREN = 4; + private static final int MATCH_SEARCH = 5; private String mAuthority; private UriMatcher mMatcher; + /** + * Implementation is provided by the parent class. + */ @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); + mMatcher.addURI(mAuthority, "root", MATCH_ROOT); + mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); + mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); + mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); + mMatcher.addURI(mAuthority, "document/*/search", MATCH_SEARCH); // Sanity check our setup if (!info.exported) { @@ -111,83 +111,80 @@ public abstract class DocumentsProvider extends ContentProvider { } /** - * 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. + * Create a new document and return its {@link Document#COLUMN_DOCUMENT_ID}. + * A provider must allocate a new {@link Document#COLUMN_DOCUMENT_ID} to + * represent the document, which must not change once returned. * - * @param docId the parent directory to create the new document under. + * @param documentId 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) + public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException { throw new UnsupportedOperationException("Create not supported"); } /** - * Rename the given document. + * Delete the given document. Upon returning, any Uri permission grants for + * the given document will be revoked. If additional documents were deleted + * as a side effect of this call, such as documents inside a directory, the + * implementor is responsible for revoking those permissions. * - * @param docId the document to rename. - * @param displayName the new display name. + * @param documentId the document to delete. */ @SuppressWarnings("unused") - public void renameDocument(String docId, String displayName) throws FileNotFoundException { - throw new UnsupportedOperationException("Rename not supported"); + public void deleteDocument(String documentId) throws FileNotFoundException { + throw new UnsupportedOperationException("Delete not supported"); } - /** - * Delete the given document. - * - * @param docId the document to delete. - */ + public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException; + @SuppressWarnings("unused") - public void deleteDocument(String docId) throws FileNotFoundException { - throw new UnsupportedOperationException("Delete not supported"); + public Cursor queryRecentDocuments(String rootId, String[] projection) + throws FileNotFoundException { + throw new UnsupportedOperationException("Recent 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. + * @param documentId the document to return. */ - public abstract Cursor queryDocument(String docId) throws FileNotFoundException; + public abstract Cursor queryDocument(String documentId, String[] projection) + throws FileNotFoundException; /** * Return the children of the given document which is a directory. * - * @param docId the directory to return children for. + * @param parentDocumentId the directory to return children for. */ - public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException; + public abstract Cursor queryChildDocuments( + String parentDocumentId, String[] projection, String sortOrder) + 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. + * @param parentDocumentId the directory to start search at. */ @SuppressWarnings("unused") - public Cursor querySearch(String docId, String query) throws FileNotFoundException { + public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection) + 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. + * {@link Document#COLUMN_MIME_TYPE} for this document. */ - public String getType(String docId) throws FileNotFoundException { - final Cursor cursor = queryDocument(docId); + public String getDocumentType(String documentId) throws FileNotFoundException { + final Cursor cursor = queryDocument(documentId, null); try { if (cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE)); + return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); } else { return null; } @@ -233,7 +230,7 @@ public abstract class DocumentsProvider extends ContentProvider { * @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 + * @see Document#FLAG_SUPPORTS_THUMBNAIL */ @SuppressWarnings("unused") public AssetFileDescriptor openDocumentThumbnail( @@ -241,17 +238,31 @@ public abstract class DocumentsProvider extends ContentProvider { throw new UnsupportedOperationException("Thumbnails not supported"); } + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #queryRoots(String[]) + * @see #queryRecentDocuments(String, String[]) + * @see #queryDocument(String, String[]) + * @see #queryChildDocuments(String, String[], String) + * @see #querySearchDocuments(String, String, String[]) + */ @Override - public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { try { switch (mMatcher.match(uri)) { + case MATCH_ROOT: + return queryRoots(projection); + case MATCH_RECENT: + return queryRecentDocuments(getRootId(uri), projection); case MATCH_DOCUMENT: - return queryDocument(getDocId(uri)); + return queryDocument(getDocumentId(uri), projection); case MATCH_CHILDREN: - return queryDocumentChildren(getDocId(uri)); + return queryChildDocuments(getDocumentId(uri), projection, sortOrder); case MATCH_SEARCH: - return querySearch(getDocId(uri), getSearchQuery(uri)); + return querySearchDocuments( + getDocumentId(uri), getSearchDocumentsQuery(uri), projection); default: throw new UnsupportedOperationException("Unsupported Uri " + uri); } @@ -261,12 +272,17 @@ public abstract class DocumentsProvider extends ContentProvider { } } + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #getDocumentType(String) + */ @Override public final String getType(Uri uri) { try { switch (mMatcher.match(uri)) { case MATCH_DOCUMENT: - return getType(getDocId(uri)); + return getDocumentType(getDocumentId(uri)); default: return null; } @@ -276,22 +292,39 @@ public abstract class DocumentsProvider extends ContentProvider { } } + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + * + * @see #createDocument(String, String, String) + */ @Override public final Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException("Insert not supported"); } + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + * + * @see #deleteDocument(String) + */ @Override public final int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Delete not supported"); } + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + */ @Override public final int update( Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Update not supported"); } + /** {@hide} */ @Override public final Bundle callFromPackage( String callingPackage, String method, String arg, Bundle extras) { @@ -300,33 +333,25 @@ public abstract class DocumentsProvider extends ContentProvider { 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"); + // Require that caller can manage given document + final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); + final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); + getContext().enforceCallingOrSelfUriPermission( + documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method); 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); + if (METHOD_CREATE_DOCUMENT.equals(method)) { + final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); + final String displayName = extras.getString(Document.COLUMN_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); + // TODO: issue Uri grant towards calling package + // TODO: enforce that package belongs to caller + final String newDocumentId = createDocument(documentId, mimeType, displayName); + out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); } else if (METHOD_DELETE_DOCUMENT.equals(method)) { - final String docId = extras.getString(DocumentColumns.DOC_ID); + final String docId = extras.getString(Document.COLUMN_DOCUMENT_ID); deleteDocument(docId); } else { @@ -338,47 +363,57 @@ public abstract class DocumentsProvider extends ContentProvider { return out; } + /** + * Implementation is provided by the parent class. + * + * @see #openDocument(String, String, CancellationSignal) + */ @Override public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - return openDocument(getDocId(uri), mode, null); + return openDocument(getDocumentId(uri), mode, null); } + /** + * Implementation is provided by the parent class. + * + * @see #openDocument(String, String, CancellationSignal) + */ @Override public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException { - return openDocument(getDocId(uri), mode, signal); + return openDocument(getDocumentId(uri), mode, signal); } + /** + * Implementation is provided by the parent class. + * + * @see #openDocumentThumbnail(String, Point, CancellationSignal) + */ @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); + return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); } else { return super.openTypedAssetFile(uri, mimeTypeFilter, opts); } } + /** + * Implementation is provided by the parent class. + * + * @see #openDocumentThumbnail(String, Point, CancellationSignal) + */ @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); + return openDocumentThumbnail(getDocumentId(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/Android.mk b/packages/DocumentsUI/Android.mk index 853353d45fff..79009532f315 100644 --- a/packages/DocumentsUI/Android.mk +++ b/packages/DocumentsUI/Android.mk @@ -5,7 +5,7 @@ LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-subdir-java-files) -LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 guava LOCAL_PACKAGE_NAME := DocumentsUI LOCAL_CERTIFICATE := platform diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml index 6cc92e3af5ba..45e26508494f 100644 --- a/packages/DocumentsUI/AndroidManifest.xml +++ b/packages/DocumentsUI/AndroidManifest.xml @@ -11,10 +11,7 @@ <!-- TODO: allow rotation when state saving is in better shape --> <activity android:name=".DocumentsActivity" - android:finishOnCloseSystemDialogs="true" - android:excludeFromRecents="true" - android:theme="@android:style/Theme.Holo.Light" - android:screenOrientation="nosensor"> + android:theme="@android:style/Theme.Holo.Light"> <intent-filter android:priority="100"> <action android:name="android.intent.action.OPEN_DOCUMENT" /> <category android:name="android.intent.category.DEFAULT" /> @@ -37,7 +34,7 @@ <intent-filter> <action android:name="android.provider.action.MANAGE_DOCUMENTS" /> <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="vnd.android.doc/dir" /> + <data android:mimeType="vnd.android.document/directory" /> </intent-filter> </activity> diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml index 928ba850fbb3..f4a822d3c4fc 100644 --- a/packages/DocumentsUI/res/values/strings.xml +++ b/packages/DocumentsUI/res/values/strings.xml @@ -63,4 +63,6 @@ <string name="more">More</string> <string name="loading">Loading\u2026</string> + <string name="share_via">Share via</string> + </resources> diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java index 6bc554f2524f..e0b8d1971a6e 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java @@ -20,7 +20,6 @@ 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.Context; import android.content.DialogInterface; @@ -28,13 +27,13 @@ import android.content.DialogInterface.OnClickListener; import android.net.Uri; import android.os.Bundle; import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Documents; +import android.provider.DocumentsContract.Document; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import android.widget.Toast; -import com.android.documentsui.model.Document; +import com.android.documentsui.model.DocumentInfo; /** * Dialog to create a new directory. @@ -67,24 +66,17 @@ public class CreateDirectoryFragment extends DialogFragment { final String displayName = text1.getText().toString(); final DocumentsActivity activity = (DocumentsActivity) getActivity(); - final Document cwd = activity.getCurrentDirectory(); + final DocumentInfo cwd = activity.getCurrentDirectory(); - final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( - cwd.uri.getAuthority()); try { - final String docId = DocumentsContract.createDocument(client, - DocumentsContract.getDocId(cwd.uri), Documents.MIME_TYPE_DIR, - displayName); + final Uri childUri = DocumentsContract.createDocument( + resolver, cwd.uri, Document.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); + final DocumentInfo childDoc = DocumentInfo.fromUri(resolver, childUri); activity.onDocumentPicked(childDoc); } 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 783b6ff53371..5b23ca5956e8 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -17,12 +17,12 @@ package com.android.documentsui; 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_DISPLAY_NAME; -import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED; -import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_SIZE; +import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE; +import static com.android.documentsui.DocumentsActivity.State.MODE_GRID; +import static com.android.documentsui.DocumentsActivity.State.MODE_LIST; +import static com.android.documentsui.model.DocumentInfo.getCursorInt; +import static com.android.documentsui.model.DocumentInfo.getCursorLong; +import static com.android.documentsui.model.DocumentInfo.getCursorString; import android.app.Fragment; import android.app.FragmentManager; @@ -32,12 +32,14 @@ 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; import android.os.AsyncTask; import android.os.Bundle; import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; import android.text.format.DateUtils; import android.text.format.Formatter; import android.text.format.Time; @@ -60,13 +62,13 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; -import com.android.documentsui.DocumentsActivity.DisplayState; -import com.android.documentsui.model.Document; +import com.android.documentsui.DocumentsActivity.State; +import com.android.documentsui.model.DocumentInfo; +import com.android.documentsui.model.RootInfo; import com.android.internal.util.Predicate; import com.google.android.collect.Lists; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -81,7 +83,7 @@ public class DirectoryFragment extends Fragment { private AbsListView mCurrentView; - private Predicate<Document> mFilter; + private Predicate<DocumentInfo> mFilter; public static final int TYPE_NORMAL = 1; public static final int TYPE_SEARCH = 2; @@ -95,30 +97,38 @@ public class DirectoryFragment extends Fragment { private LoaderCallbacks<DirectoryResult> mCallbacks; private static final String EXTRA_TYPE = "type"; - private static final String EXTRA_URI = "uri"; + private static final String EXTRA_AUTHORITY = "authority"; + private static final String EXTRA_ROOT_ID = "rootId"; + private static final String EXTRA_DOC_ID = "docId"; + private static final String EXTRA_QUERY = "query"; private static AtomicInteger sLoaderId = new AtomicInteger(4000); + private int mLastSortOrder = -1; + private final int mLoaderId = sLoaderId.incrementAndGet(); public static void showNormal(FragmentManager fm, Uri uri) { - show(fm, TYPE_NORMAL, uri); + show(fm, TYPE_NORMAL, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), null); } public static void showSearch(FragmentManager fm, Uri uri, String query) { - final Uri searchUri = DocumentsContract.buildSearchUri( - uri.getAuthority(), DocumentsContract.getDocId(uri), query); - show(fm, TYPE_SEARCH, searchUri); + show(fm, TYPE_SEARCH, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), + query); } public static void showRecentsOpen(FragmentManager fm) { - show(fm, TYPE_RECENT_OPEN, null); + show(fm, TYPE_RECENT_OPEN, null, null, null, null); } - private static void show(FragmentManager fm, int type, Uri uri) { + private static void show(FragmentManager fm, int type, String authority, String rootId, + String docId, String query) { final Bundle args = new Bundle(); args.putInt(EXTRA_TYPE, type); - args.putParcelable(EXTRA_URI, uri); + args.putString(EXTRA_AUTHORITY, authority); + args.putString(EXTRA_ROOT_ID, rootId); + args.putString(EXTRA_DOC_ID, docId); + args.putString(EXTRA_QUERY, query); final DirectoryFragment fragment = new DirectoryFragment(); fragment.setArguments(args); @@ -137,7 +147,6 @@ public class DirectoryFragment extends Fragment { public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final Context context = inflater.getContext(); - final View view = inflater.inflate(R.layout.fragment_directory, container, false); mEmptyView = view.findViewById(android.R.id.empty); @@ -150,80 +159,77 @@ public class DirectoryFragment extends Fragment { mGridView.setOnItemClickListener(mItemListener); mGridView.setMultiChoiceModeListener(mMultiListener); - mAdapter = new DocumentsAdapter(); + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - final Uri uri = getArguments().getParcelable(EXTRA_URI); + final Context context = getActivity(); + + mAdapter = new DocumentsAdapter(); mType = getArguments().getInt(EXTRA_TYPE); mCallbacks = new LoaderCallbacks<DirectoryResult>() { @Override public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { - final DisplayState state = getDisplayState(DirectoryFragment.this); - mFilter = new MimePredicate(state.acceptMimes); + final State state = getDisplayState(DirectoryFragment.this); + + final String authority = getArguments().getString(EXTRA_AUTHORITY); + final String rootId = getArguments().getString(EXTRA_ROOT_ID); + final String docId = getArguments().getString(EXTRA_DOC_ID); + final String query = getArguments().getString(EXTRA_QUERY); Uri contentsUri; - if (mType == TYPE_NORMAL) { - contentsUri = DocumentsContract.buildChildrenUri( - uri.getAuthority(), DocumentsContract.getDocId(uri)); - } else if (mType == TYPE_RECENT_OPEN) { - contentsUri = RecentsProvider.buildRecentOpen(); - } else { - contentsUri = uri; - } + switch (mType) { + case TYPE_NORMAL: + contentsUri = DocumentsContract.buildChildDocumentsUri(authority, docId); + return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder); + case TYPE_SEARCH: + contentsUri = DocumentsContract.buildSearchDocumentsUri( + authority, docId, query); + return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder); + case TYPE_RECENT_OPEN: + final RootsCache roots = DocumentsApplication.getRootsCache(context); + final List<RootInfo> matchingRoots = roots.getMatchingRoots(state); + return new RecentLoader(context, matchingRoots); + default: + throw new IllegalStateException("Unknown type " + mType); - final Comparator<Document> sortOrder; - 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 { - throw new IllegalArgumentException("Unknown sort order " + state.sortOrder); } - - return new DirectoryLoader(context, contentsUri, mType, null, sortOrder); } @Override public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { - mAdapter.swapDocuments(result.contents); + mAdapter.swapCursor(result.cursor); } @Override public void onLoaderReset(Loader<DirectoryResult> loader) { - mAdapter.swapDocuments(null); + mAdapter.swapCursor(null); } }; updateDisplayState(); - - return view; - } - - @Override - public void onStart() { - super.onStart(); - getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks); - } - - @Override - public void onStop() { - super.onStop(); - getLoaderManager().destroyLoader(mLoaderId); } public void updateDisplayState() { - final DisplayState state = getDisplayState(this); + final State state = getDisplayState(this); + + if (mLastSortOrder != state.sortOrder) { + getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); + mLastSortOrder = state.sortOrder; + } - // TODO: avoid kicking loader when nothing changed - getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks); mListView.smoothScrollToPosition(0); mGridView.smoothScrollToPosition(0); mListView.setVisibility(state.mode == MODE_LIST ? View.VISIBLE : View.GONE); mGridView.setVisibility(state.mode == MODE_GRID ? View.VISIBLE : View.GONE); + mFilter = new MimePredicate(state.acceptMimes); + final int choiceMode; if (state.allowMultiple) { choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; @@ -258,7 +264,8 @@ public class DirectoryFragment extends Fragment { private OnItemClickListener mItemListener = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - final Document doc = mAdapter.getItem(position); + final Cursor cursor = mAdapter.getItem(position); + final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); if (mFilter.apply(doc)) { ((DocumentsActivity) getActivity()).onDocumentPicked(doc); } @@ -274,7 +281,7 @@ public class DirectoryFragment extends Fragment { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - final DisplayState state = getDisplayState(DirectoryFragment.this); + final State state = getDisplayState(DirectoryFragment.this); final MenuItem open = menu.findItem(R.id.menu_open); final MenuItem share = menu.findItem(R.id.menu_share); @@ -291,11 +298,12 @@ public class DirectoryFragment extends Fragment { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); - final ArrayList<Document> docs = Lists.newArrayList(); + final ArrayList<DocumentInfo> docs = Lists.newArrayList(); final int size = checked.size(); for (int i = 0; i < size; i++) { if (checked.valueAt(i)) { - final Document doc = mAdapter.getItem(checked.keyAt(i)); + final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); + final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); docs.add(doc); } } @@ -303,14 +311,17 @@ public class DirectoryFragment extends Fragment { final int id = item.getItemId(); if (id == R.id.menu_open) { DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); + mode.finish(); return true; } else if (id == R.id.menu_share) { onShareDocuments(docs); + mode.finish(); return true; } else if (id == R.id.menu_delete) { onDeleteDocuments(docs); + mode.finish(); return true; } else { @@ -328,8 +339,9 @@ public class DirectoryFragment extends Fragment { ActionMode mode, int position, long id, boolean checked) { if (checked) { // Directories cannot be checked - final Document doc = mAdapter.getItem(position); - if (doc.isDirectory()) { + final Cursor cursor = mAdapter.getItem(position); + final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + if (Document.MIME_TYPE_DIR.equals(docMimeType)) { mCurrentView.setItemChecked(position, false); } } @@ -339,36 +351,46 @@ public class DirectoryFragment extends Fragment { } }; - private void onShareDocuments(List<Document> docs) { - final ArrayList<Uri> uris = Lists.newArrayList(); - for (Document doc : docs) { - uris.add(doc.uri); - } + private void onShareDocuments(List<DocumentInfo> docs) { + Intent intent; + if (docs.size() == 1) { + final DocumentInfo doc = docs.get(0); - final Intent intent; - if (uris.size() > 1) { + intent = new Intent(Intent.ACTION_SEND); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setType(doc.mimeType); + intent.putExtra(Intent.EXTRA_STREAM, doc.uri); + + } else if (docs.size() > 1) { intent = new Intent(Intent.ACTION_SEND_MULTIPLE); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addCategory(Intent.CATEGORY_DEFAULT); - // TODO: find common mimetype - intent.setType("*/*"); + + final ArrayList<String> mimeTypes = Lists.newArrayList(); + final ArrayList<Uri> uris = Lists.newArrayList(); + for (DocumentInfo doc : docs) { + mimeTypes.add(doc.mimeType); + uris.add(doc.uri); + } + + intent.setType(findCommonMimeType(mimeTypes)); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { - intent = new Intent(Intent.ACTION_SEND); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(uris.get(0)); + return; } + intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); startActivity(intent); } - private void onDeleteDocuments(List<Document> docs) { + private void onDeleteDocuments(List<DocumentInfo> docs) { final Context context = getActivity(); final ContentResolver resolver = context.getContentResolver(); boolean hadTrouble = false; - for (Document doc : docs) { + for (DocumentInfo doc : docs) { if (!doc.isDeleteSupported()) { Log.w(TAG, "Skipping " + doc); hadTrouble = true; @@ -391,20 +413,17 @@ public class DirectoryFragment extends Fragment { } } - private static DisplayState getDisplayState(Fragment fragment) { + private static State getDisplayState(Fragment fragment) { return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); } private class DocumentsAdapter extends BaseAdapter { - private List<Document> mDocuments; - - public DocumentsAdapter() { - } + private Cursor mCursor; - public void swapDocuments(List<Document> documents) { - mDocuments = documents; + public void swapCursor(Cursor cursor) { + mCursor = cursor; - if (mDocuments != null && mDocuments.isEmpty()) { + if (isEmpty()) { mEmptyView.setVisibility(View.VISIBLE); } else { mEmptyView.setVisibility(View.GONE); @@ -416,7 +435,7 @@ public class DirectoryFragment extends Fragment { @Override public View getView(int position, View convertView, ViewGroup parent) { final Context context = parent.getContext(); - final DisplayState state = getDisplayState(DirectoryFragment.this); + final State state = getDisplayState(DirectoryFragment.this); final RootsCache roots = DocumentsApplication.getRootsCache(context); final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( @@ -433,7 +452,18 @@ public class DirectoryFragment extends Fragment { } } - final Document doc = getItem(position); + final Cursor cursor = getItem(position); + + final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); + final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); + final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); + final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); + final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); + final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); + final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); + final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); + final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); final TextView title = (TextView) convertView.findViewById(android.R.id.title); @@ -448,32 +478,39 @@ public class DirectoryFragment extends Fragment { oldTask.cancel(false); } - if (doc.isThumbnailSupported()) { - final Bitmap cachedResult = thumbs.get(doc.uri); + if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) { + final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); + final Bitmap cachedResult = thumbs.get(uri); if (cachedResult != null) { icon.setImageBitmap(cachedResult); } else { final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize); icon.setImageBitmap(null); icon.setTag(task); - task.execute(doc.uri); + task.execute(uri); } + } else if (docIcon != 0) { + icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon)); } else { - icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, doc.mimeType)); + icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType)); } - title.setText(doc.displayName); + title.setText(docDisplayName); - if (mType == TYPE_NORMAL || mType == TYPE_SEARCH) { + if (mType == TYPE_RECENT_OPEN) { + final RootInfo root = roots.getRoot(docAuthority, docRootId); + icon1.setVisibility(View.VISIBLE); + icon1.setImageDrawable(root.loadIcon(context)); + summary.setText(root.getDirectoryString()); + summary.setVisibility(View.VISIBLE); + } else { icon1.setVisibility(View.GONE); - if (doc.summary != null) { - summary.setText(doc.summary); + if (docSummary != null) { + summary.setText(docSummary); summary.setVisibility(View.VISIBLE); } else { summary.setVisibility(View.INVISIBLE); } - } else if (mType == TYPE_RECENT_OPEN) { - // TODO: resolve storage root } if (summaryGrid != null) { @@ -481,18 +518,18 @@ public class DirectoryFragment extends Fragment { (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE); } - if (doc.lastModified == -1) { + if (docLastModified == -1) { date.setText(null); } else { - date.setText(formatTime(context, doc.lastModified)); + date.setText(formatTime(context, docLastModified)); } if (state.showSize) { size.setVisibility(View.VISIBLE); - if (doc.isDirectory() || doc.size == -1) { + if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { size.setText(null); } else { - size.setText(Formatter.formatFileSize(context, doc.size)); + size.setText(Formatter.formatFileSize(context, docSize)); } } else { size.setVisibility(View.GONE); @@ -503,17 +540,20 @@ public class DirectoryFragment extends Fragment { @Override public int getCount() { - return mDocuments != null ? mDocuments.size() : 0; + return mCursor != null ? mCursor.getCount() : 0; } @Override - public Document getItem(int position) { - return mDocuments.get(position); + public Cursor getItem(int position) { + if (mCursor != null) { + mCursor.moveToPosition(position); + } + return mCursor; } @Override public long getItemId(int position) { - return getItem(position).uri.hashCode(); + return position; } } @@ -538,8 +578,8 @@ public class DirectoryFragment extends Fragment { Bitmap result = null; try { - result = DocumentsContract.getThumbnail( - context.getContentResolver(), uri, mThumbSize); + result = DocumentsContract.getDocumentThumbnail( + context.getContentResolver(), uri, mThumbSize, null); if (result != null) { final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( context, mThumbSize); @@ -580,4 +620,28 @@ public class DirectoryFragment extends Fragment { return DateUtils.formatDateTime(context, when, flags); } + + private String findCommonMimeType(List<String> mimeTypes) { + String[] commonType = mimeTypes.get(0).split("/"); + if (commonType.length != 2) { + return "*/*"; + } + + for (int i = 1; i < mimeTypes.size(); i++) { + String[] type = mimeTypes.get(i).split("/"); + if (type.length != 2) continue; + + if (!commonType[1].equals(type[1])) { + commonType[1] = "*"; + } + + if (!commonType[0].equals(type[0])) { + commonType[0] = "*"; + commonType[1] = "*"; + break; + } + } + + return commonType[0] + "/" + commonType[1]; + } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java index 4ce5ef8c23cf..3f016b50143f 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java @@ -16,98 +16,155 @@ package com.android.documentsui; -import static com.android.documentsui.DirectoryFragment.TYPE_NORMAL; -import static com.android.documentsui.DirectoryFragment.TYPE_RECENT_OPEN; -import static com.android.documentsui.DirectoryFragment.TYPE_SEARCH; -import static com.android.documentsui.DocumentsActivity.TAG; +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME; +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED; +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE; -import android.content.ContentResolver; +import android.content.AsyncTaskLoader; +import android.content.ContentProviderClient; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; -import android.util.Log; - -import com.android.documentsui.model.Document; -import com.android.internal.util.Predicate; -import com.google.android.collect.Lists; +import android.os.OperationCanceledException; +import android.provider.DocumentsContract.Document; import libcore.io.IoUtils; -import java.io.FileNotFoundException; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - class DirectoryResult implements AutoCloseable { + ContentProviderClient client; Cursor cursor; - List<Document> contents = Lists.newArrayList(); - Exception e; + Exception exception; @Override - public void close() throws Exception { + public void close() { IoUtils.closeQuietly(cursor); + ContentProviderClient.closeQuietly(client); + cursor = null; + client = null; } } -public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> { +public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { + private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver(); + + private final String mRootId; + private final Uri mUri; + private final int mSortOrder; - private final int mType; - private Predicate<Document> mFilter; - private Comparator<Document> mSortOrder; + private CancellationSignal mSignal; + private DirectoryResult mResult; - public DirectoryLoader(Context context, Uri uri, int type, Predicate<Document> filter, - Comparator<Document> sortOrder) { - super(context, uri); - mType = type; - mFilter = filter; + public DirectoryLoader(Context context, String rootId, Uri uri, int sortOrder) { + super(context); + mRootId = rootId; + mUri = uri; mSortOrder = sortOrder; } @Override - public DirectoryResult loadInBackground(Uri uri, CancellationSignal signal) { + public final DirectoryResult loadInBackground() { + synchronized (this) { + if (isLoadInBackgroundCanceled()) { + throw new OperationCanceledException(); + } + mSignal = new CancellationSignal(); + } final DirectoryResult result = new DirectoryResult(); + final String authority = mUri.getAuthority(); try { - loadInBackgroundInternal(result, uri, signal); + result.client = getContext() + .getContentResolver().acquireUnstableContentProviderClient(authority); + final Cursor cursor = result.client.query( + mUri, null, null, null, getQuerySortOrder(mSortOrder), mSignal); + final Cursor withRoot = new RootCursorWrapper(mUri.getAuthority(), mRootId, cursor, -1); + final Cursor sorted = new SortingCursorWrapper(withRoot, mSortOrder); + + result.cursor = sorted; + result.cursor.registerContentObserver(mObserver); } catch (Exception e) { - result.e = e; + result.exception = e; + ContentProviderClient.closeQuietly(result.client); + } finally { + synchronized (this) { + mSignal = null; + } } return result; } - private void loadInBackgroundInternal( - 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, null, signal); - result.cursor = cursor; - result.cursor.registerContentObserver(mObserver); - - while (cursor.moveToNext()) { - Document doc = null; - switch (mType) { - case TYPE_NORMAL: - case TYPE_SEARCH: - doc = Document.fromDirectoryCursor(uri, cursor); - break; - case TYPE_RECENT_OPEN: - try { - doc = Document.fromRecentOpenCursor(resolver, cursor); - } catch (FileNotFoundException e) { - Log.w(TAG, "Failed to find recent: " + e); - } - break; - default: - throw new IllegalArgumentException("Unknown type"); - } + @Override + public void cancelLoadInBackground() { + super.cancelLoadInBackground(); - if (doc != null && (mFilter == null || mFilter.apply(doc))) { - result.contents.add(doc); + synchronized (this) { + if (mSignal != null) { + mSignal.cancel(); } } + } + + @Override + public void deliverResult(DirectoryResult result) { + if (isReset()) { + IoUtils.closeQuietly(result); + return; + } + DirectoryResult oldResult = mResult; + mResult = result; + + if (isStarted()) { + super.deliverResult(result); + } + + if (oldResult != null && oldResult != result) { + IoUtils.closeQuietly(oldResult); + } + } + + @Override + protected void onStartLoading() { + if (mResult != null) { + deliverResult(mResult); + } + if (takeContentChanged() || mResult == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void onCanceled(DirectoryResult result) { + IoUtils.closeQuietly(result); + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + IoUtils.closeQuietly(mResult); + mResult = null; + + getContext().getContentResolver().unregisterContentObserver(mObserver); + } - if (mSortOrder != null) { - Collections.sort(result.contents, mSortOrder); + public static String getQuerySortOrder(int sortOrder) { + switch (sortOrder) { + case SORT_ORDER_DISPLAY_NAME: + return Document.COLUMN_DISPLAY_NAME + " ASC"; + case SORT_ORDER_LAST_MODIFIED: + return Document.COLUMN_LAST_MODIFIED + " DESC"; + case SORT_ORDER_SIZE: + return Document.COLUMN_SIZE + " DESC"; + default: + return null; } } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java index 0ce5968847f4..54f62ef56ebe 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java @@ -21,11 +21,10 @@ 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; /** - * Handles {@link DocumentRoot} changes which invalidate cached data. + * Handles 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 73ca8fa335f6..f569f5ae3e84 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java @@ -16,13 +16,13 @@ package com.android.documentsui; -import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_CREATE; -import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_GET_CONTENT; -import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE; -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_LAST_MODIFIED; +import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE; +import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT; +import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE; +import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN; +import static com.android.documentsui.DocumentsActivity.State.MODE_GRID; +import static com.android.documentsui.DocumentsActivity.State.MODE_LIST; +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED; import android.app.ActionBar; import android.app.ActionBar.OnNavigationListener; @@ -41,8 +41,8 @@ import android.database.Cursor; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Bundle; +import android.os.Parcel; import android.provider.DocumentsContract; -import android.provider.DocumentsContract.DocumentRoot; import android.support.v4.app.ActionBarDrawerToggle; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; @@ -60,32 +60,29 @@ import android.widget.SearchView.OnQueryTextListener; import android.widget.TextView; import android.widget.Toast; -import com.android.documentsui.model.Document; +import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; +import com.android.documentsui.model.DurableUtils; +import com.android.documentsui.model.RootInfo; import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Arrays; import java.util.List; public class DocumentsActivity extends Activity { public static final String TAG = "Documents"; - private int mAction; - private SearchView mSearchView; private View mRootsContainer; private DrawerLayout mDrawerLayout; private ActionBarDrawerToggle mDrawerToggle; - private final DisplayState mDisplayState = new DisplayState(); + private static final String EXTRA_STATE = "state"; private RootsCache mRoots; - - /** Current user navigation stack; empty implies recents. */ - private DocumentStack mStack = new DocumentStack(); - /** Currently active search, overriding any stack. */ - private String mCurrentSearch; + private State mState; @Override public void onCreate(Bundle icicle) { @@ -93,74 +90,86 @@ public class DocumentsActivity extends Activity { mRoots = DocumentsApplication.getRootsCache(this); - final Intent intent = getIntent(); - final String action = intent.getAction(); - if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { - mAction = ACTION_OPEN; - } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { - mAction = ACTION_CREATE; - } else if (Intent.ACTION_GET_CONTENT.equals(action)) { - mAction = ACTION_GET_CONTENT; - } else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) { - mAction = ACTION_MANAGE; - } + setResult(Activity.RESULT_CANCELED); + setContentView(R.layout.activity); - // TODO: unify action in single place - mDisplayState.action = mAction; + mRootsContainer = findViewById(R.id.container_roots); - if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) { - mDisplayState.allowMultiple = intent.getBooleanExtra( - Intent.EXTRA_ALLOW_MULTIPLE, false); - } + mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - if (mAction == ACTION_MANAGE) { - mDisplayState.acceptMimes = new String[] { "*/*" }; - mDisplayState.allowMultiple = true; - } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) { - mDisplayState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES); + mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, + R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close); + + mDrawerLayout.setDrawerListener(mDrawerListener); + mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + + if (icicle != null) { + mState = icicle.getParcelable(EXTRA_STATE); } else { - mDisplayState.acceptMimes = new String[] { intent.getType() }; + buildDefaultState(); } - mDisplayState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false); - - setResult(Activity.RESULT_CANCELED); - setContentView(R.layout.activity); + if (mState.action == ACTION_MANAGE) { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + } - if (mAction == ACTION_CREATE) { + if (mState.action == ACTION_CREATE) { final String mimeType = getIntent().getType(); final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); SaveFragment.show(getFragmentManager(), mimeType, title); } - if (mAction == ACTION_GET_CONTENT) { + if (mState.action == ACTION_GET_CONTENT) { final Intent moreApps = new Intent(getIntent()); moreApps.setComponent(null); moreApps.setPackage(null); RootsFragment.show(getFragmentManager(), moreApps); - } else if (mAction == ACTION_OPEN || mAction == ACTION_CREATE) { + } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE) { RootsFragment.show(getFragmentManager(), null); } - if (mAction == ACTION_MANAGE) { - mDisplayState.sortOrder = SORT_ORDER_LAST_MODIFIED; - } + onCurrentDirectoryChanged(); + } - mRootsContainer = findViewById(R.id.container_roots); + private void buildDefaultState() { + mState = new State(); - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + final Intent intent = getIntent(); + final String action = intent.getAction(); + if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { + mState.action = ACTION_OPEN; + } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { + mState.action = ACTION_CREATE; + } else if (Intent.ACTION_GET_CONTENT.equals(action)) { + mState.action = ACTION_GET_CONTENT; + } else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) { + mState.action = ACTION_MANAGE; + } - mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, - R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close); + if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { + mState.allowMultiple = intent.getBooleanExtra( + Intent.EXTRA_ALLOW_MULTIPLE, false); + } - mDrawerLayout.setDrawerListener(mDrawerListener); - mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + if (mState.action == ACTION_MANAGE) { + mState.acceptMimes = new String[] { "*/*" }; + mState.allowMultiple = true; + } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) { + mState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES); + } else { + mState.acceptMimes = new String[] { intent.getType() }; + } - if (mAction == ACTION_MANAGE) { - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false); + mState.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(this); + + if (mState.action == ACTION_MANAGE) { + mState.sortOrder = SORT_ORDER_LAST_MODIFIED; + } + if (mState.action == ACTION_MANAGE) { final Uri rootUri = intent.getData(); - final DocumentRoot root = mRoots.findRoot(rootUri); + final RootInfo root = mRoots.findRoot(rootUri); if (root != null) { onRootPicked(root, true); } else { @@ -169,8 +178,6 @@ public class DocumentsActivity extends Activity { } } else { - mDrawerLayout.openDrawer(mRootsContainer); - // Restore last stack for calling package // TODO: move into async loader final String packageName = getCallingPackage(); @@ -178,17 +185,17 @@ public class DocumentsActivity extends Activity { .query(RecentsProvider.buildResume(packageName), null, null, null, null); try { if (cursor.moveToFirst()) { - final String raw = cursor.getString( + final byte[] rawStack = cursor.getBlob( cursor.getColumnIndex(RecentsProvider.COL_PATH)); - mStack = DocumentStack.deserialize(getContentResolver(), raw); + DurableUtils.readFromArray(rawStack, mState.stack); } - } catch (FileNotFoundException e) { + } catch (IOException e) { Log.w(TAG, "Failed to resume", e); } finally { cursor.close(); } - onCurrentDirectoryChanged(); + mDrawerLayout.openDrawer(mRootsContainer); } } @@ -196,10 +203,10 @@ public class DocumentsActivity extends Activity { public void onStart() { super.onStart(); - if (mAction == ACTION_MANAGE) { - mDisplayState.showSize = true; + if (mState.action == ACTION_MANAGE) { + mState.showSize = true; } else { - mDisplayState.showSize = SettingsActivity.getDisplayFileSize(this); + mState.showSize = SettingsActivity.getDisplayFileSize(this); } } @@ -242,9 +249,9 @@ public class DocumentsActivity extends Activity { actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); actionBar.setIcon(new ColorDrawable()); - if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) { + if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { actionBar.setTitle(R.string.title_open); - } else if (mAction == ACTION_CREATE) { + } else if (mState.action == ACTION_CREATE) { actionBar.setTitle(R.string.title_save); } @@ -252,7 +259,7 @@ public class DocumentsActivity extends Activity { mDrawerToggle.setDrawerIndicatorEnabled(true); } else { - final DocumentRoot root = getCurrentRoot(); + final RootInfo root = getCurrentRoot(); actionBar.setIcon(root != null ? root.loadIcon(this) : null); if (mRoots.isRecentsRoot(root)) { @@ -262,13 +269,13 @@ public class DocumentsActivity extends Activity { actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); actionBar.setTitle(null); actionBar.setListNavigationCallbacks(mSortAdapter, mSortListener); - actionBar.setSelectedNavigationItem(mDisplayState.sortOrder); + actionBar.setSelectedNavigationItem(mState.sortOrder); } - if (mStack.size() > 1) { + if (mState.stack.size() > 1) { actionBar.setDisplayHomeAsUpEnabled(true); mDrawerToggle.setDrawerIndicatorEnabled(false); - } else if (mAction == ACTION_MANAGE) { + } else if (mState.action == ACTION_MANAGE) { actionBar.setDisplayHomeAsUpEnabled(false); mDrawerToggle.setDrawerIndicatorEnabled(false); } else { @@ -288,7 +295,7 @@ public class DocumentsActivity extends Activity { mSearchView.setOnQueryTextListener(new OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { - mCurrentSearch = query; + mState.currentSearch = query; onCurrentDirectoryChanged(); mSearchView.setIconified(true); return true; @@ -303,7 +310,7 @@ public class DocumentsActivity extends Activity { mSearchView.setOnCloseListener(new OnCloseListener() { @Override public boolean onClose() { - mCurrentSearch = null; + mState.currentSearch = null; onCurrentDirectoryChanged(); return false; } @@ -317,7 +324,7 @@ public class DocumentsActivity extends Activity { super.onPrepareOptionsMenu(menu); final FragmentManager fm = getFragmentManager(); - final Document cwd = getCurrentDirectory(); + final DocumentInfo cwd = getCurrentDirectory(); final MenuItem createDir = menu.findItem(R.id.menu_create_dir); final MenuItem search = menu.findItem(R.id.menu_search); @@ -325,11 +332,11 @@ public class DocumentsActivity extends Activity { final MenuItem list = menu.findItem(R.id.menu_list); final MenuItem settings = menu.findItem(R.id.menu_settings); - grid.setVisible(mDisplayState.mode != MODE_GRID); - list.setVisible(mDisplayState.mode != MODE_LIST); + grid.setVisible(mState.mode != MODE_GRID); + list.setVisible(mState.mode != MODE_LIST); final boolean searchVisible; - if (mAction == ACTION_CREATE) { + if (mState.action == ACTION_CREATE) { createDir.setVisible(cwd != null && cwd.isCreateSupported()); searchVisible = false; @@ -348,7 +355,7 @@ public class DocumentsActivity extends Activity { // TODO: close any search in-progress when hiding search.setVisible(searchVisible); - settings.setVisible(mAction != ACTION_MANAGE); + settings.setVisible(mState.action != ACTION_MANAGE); return true; } @@ -370,13 +377,13 @@ public class DocumentsActivity extends Activity { return false; } else if (id == R.id.menu_grid) { // TODO: persist explicit user mode for cwd - mDisplayState.mode = MODE_GRID; + mState.mode = MODE_GRID; updateDisplayState(); invalidateOptionsMenu(); return true; } else if (id == R.id.menu_list) { // TODO: persist explicit user mode for cwd - mDisplayState.mode = MODE_LIST; + mState.mode = MODE_LIST; updateDisplayState(); invalidateOptionsMenu(); return true; @@ -390,9 +397,9 @@ public class DocumentsActivity extends Activity { @Override public void onBackPressed() { - final int size = mStack.size(); + final int size = mState.stack.size(); if (size > 1) { - mStack.pop(); + mState.stack.pop(); onCurrentDirectoryChanged(); } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsContainer)) { // TODO: open root drawer once we can capture back key @@ -402,11 +409,23 @@ public class DocumentsActivity extends Activity { } } + @Override + protected void onSaveInstanceState(Bundle state) { + super.onSaveInstanceState(state); + state.putParcelable(EXTRA_STATE, mState); + } + + @Override + protected void onRestoreInstanceState(Bundle state) { + super.onRestoreInstanceState(state); + updateActionBar(); + } + // TODO: support additional sort orders private BaseAdapter mSortAdapter = new BaseAdapter() { @Override public int getCount() { - return mDisplayState.showSize ? 3 : 2; + return mState.showSize ? 3 : 2; } @Override @@ -438,8 +457,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); - if (mStack.size() > 0) { - title.setText(mStack.getTitle(mRoots)); + if (mState.stack.size() > 0) { + title.setText(mState.stack.getTitle(mRoots)); } else { // No directory means recents title.setText(R.string.root_recent); @@ -467,43 +486,43 @@ public class DocumentsActivity extends Activity { private OnNavigationListener mSortListener = new OnNavigationListener() { @Override public boolean onNavigationItemSelected(int itemPosition, long itemId) { - mDisplayState.sortOrder = itemPosition; + mState.sortOrder = itemPosition; updateDisplayState(); return true; } }; - public DocumentRoot getCurrentRoot() { - if (mStack.size() > 0) { - return mStack.getRoot(mRoots); + public RootInfo getCurrentRoot() { + if (mState.stack.size() > 0) { + return mState.stack.getRoot(mRoots); } else { return mRoots.getRecentsRoot(); } } - public Document getCurrentDirectory() { - return mStack.peek(); + public DocumentInfo getCurrentDirectory() { + return mState.stack.peek(); } - public DisplayState getDisplayState() { - return mDisplayState; + public State getDisplayState() { + return mState; } private void onCurrentDirectoryChanged() { final FragmentManager fm = getFragmentManager(); - final Document cwd = getCurrentDirectory(); + final DocumentInfo cwd = getCurrentDirectory(); if (cwd == null) { // No directory means recents - if (mAction == ACTION_CREATE) { + if (mState.action == ACTION_CREATE) { RecentsCreateFragment.show(fm); } else { DirectoryFragment.showRecentsOpen(fm); } } else { - if (mCurrentSearch != null) { + if (mState.currentSearch != null) { // Ongoing search - DirectoryFragment.showSearch(fm, cwd.uri, mCurrentSearch); + DirectoryFragment.showSearch(fm, cwd.uri, mState.currentSearch); } else { // Normal boring directory DirectoryFragment.showNormal(fm, cwd.uri); @@ -511,7 +530,7 @@ public class DocumentsActivity extends Activity { } // Forget any replacement target - if (mAction == ACTION_CREATE) { + if (mState.action == ACTION_CREATE) { final SaveFragment save = SaveFragment.get(fm); if (save != null) { save.setReplaceTarget(null); @@ -529,18 +548,18 @@ public class DocumentsActivity extends Activity { } public void onStackPicked(DocumentStack stack) { - mStack = stack; + mState.stack = stack; onCurrentDirectoryChanged(); } - public void onRootPicked(DocumentRoot root, boolean closeDrawer) { + public void onRootPicked(RootInfo root, boolean closeDrawer) { // Clear entire backstack and start in new root - mStack.clear(); + mState.stack.clear(); if (!mRoots.isRecentsRoot(root)) { try { - final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.docId); - onDocumentPicked(Document.fromUri(getContentResolver(), uri)); + final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.documentId); + onDocumentPicked(DocumentInfo.fromUri(getContentResolver(), uri)); } catch (FileNotFoundException e) { } } else { @@ -561,24 +580,24 @@ public class DocumentsActivity extends Activity { finish(); } - public void onDocumentPicked(Document doc) { + public void onDocumentPicked(DocumentInfo doc) { final FragmentManager fm = getFragmentManager(); if (doc.isDirectory()) { // TODO: query display mode user preference for this dir if (doc.isGridPreferred()) { - mDisplayState.mode = MODE_GRID; + mState.mode = MODE_GRID; } else { - mDisplayState.mode = MODE_LIST; + mState.mode = MODE_LIST; } - mStack.push(doc); + mState.stack.push(doc); onCurrentDirectoryChanged(); - } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) { + } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { // Explicit file picked, return onFinished(doc.uri); - } else if (mAction == ACTION_CREATE) { + } else if (mState.action == ACTION_CREATE) { // Replace selected file SaveFragment.get(fm).setReplaceTarget(doc); - } else if (mAction == ACTION_MANAGE) { + } else if (mState.action == ACTION_MANAGE) { // Open the document final Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -591,8 +610,8 @@ public class DocumentsActivity extends Activity { } } - public void onDocumentsPicked(List<Document> docs) { - if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) { + public void onDocumentsPicked(List<DocumentInfo> docs) { + if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { final int size = docs.size(); final Uri[] uris = new Uri[size]; for (int i = 0; i < size; i++) { @@ -602,21 +621,19 @@ public class DocumentsActivity extends Activity { } } - public void onSaveRequested(Document replaceTarget) { + public void onSaveRequested(DocumentInfo replaceTarget) { onFinished(replaceTarget.uri); } public void onSaveRequested(String mimeType, String displayName) { - final Document cwd = getCurrentDirectory(); + final DocumentInfo cwd = getCurrentDirectory(); 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); + final Uri childUri = DocumentsContract.createDocument( + getContentResolver(), cwd.uri, mimeType, displayName); onFinished(childUri); } catch (Exception e) { Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show(); @@ -631,14 +648,14 @@ public class DocumentsActivity extends Activity { final ContentResolver resolver = getContentResolver(); final ContentValues values = new ContentValues(); - final String rawStack = DocumentStack.serialize(mStack); - if (mAction == ACTION_CREATE) { + final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack); + if (mState.action == ACTION_CREATE) { // Remember stack for last create values.clear(); values.put(RecentsProvider.COL_PATH, rawStack); resolver.insert(RecentsProvider.buildRecentCreate(), values); - } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) { + } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { // Remember opened items for (Uri uri : uris) { values.clear(); @@ -658,14 +675,14 @@ public class DocumentsActivity extends Activity { intent.setData(uris[0]); } else if (uris.length > 1) { final ClipData clipData = new ClipData( - null, mDisplayState.acceptMimes, new ClipData.Item(uris[0])); + null, mState.acceptMimes, new ClipData.Item(uris[0])); for (int i = 1; i < uris.length; i++) { clipData.addItem(new ClipData.Item(uris[i])); } intent.setClipData(clipData); } - if (mAction == ACTION_GET_CONTENT) { + if (mState.action == ACTION_GET_CONTENT) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION @@ -677,7 +694,7 @@ public class DocumentsActivity extends Activity { finish(); } - public static class DisplayState { + public static class State implements android.os.Parcelable { public int action; public int mode = MODE_LIST; public String[] acceptMimes; @@ -685,6 +702,12 @@ public class DocumentsActivity extends Activity { public boolean allowMultiple = false; public boolean showSize = false; public boolean localOnly = false; + public boolean showAdvanced = false; + + /** Current user navigation stack; empty implies recents. */ + public DocumentStack stack = new DocumentStack(); + /** Currently active search, overriding any stack. */ + public String currentSearch; public static final int ACTION_OPEN = 1; public static final int ACTION_CREATE = 2; @@ -697,11 +720,53 @@ public class DocumentsActivity extends Activity { 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; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(action); + out.writeInt(mode); + out.writeStringArray(acceptMimes); + out.writeInt(sortOrder); + out.writeInt(allowMultiple ? 1 : 0); + out.writeInt(showSize ? 1 : 0); + out.writeInt(localOnly ? 1 : 0); + out.writeInt(showAdvanced ? 1 : 0); + DurableUtils.writeToParcel(out, stack); + out.writeString(currentSearch); + } + + public static final Creator<State> CREATOR = new Creator<State>() { + @Override + public State createFromParcel(Parcel in) { + final State state = new State(); + state.action = in.readInt(); + state.mode = in.readInt(); + state.acceptMimes = in.readStringArray(); + state.sortOrder = in.readInt(); + state.allowMultiple = in.readInt() != 0; + state.showSize = in.readInt() != 0; + state.localOnly = in.readInt() != 0; + state.showAdvanced = in.readInt() != 0; + DurableUtils.readFromParcel(in, state.stack); + state.currentSearch = in.readString(); + return state; + } + + @Override + public State[] newArray(int size) { + return new State[size]; + } + }; } private void dumpStack() { Log.d(TAG, "Current stack:"); - for (Document doc : mStack) { + for (DocumentInfo doc : mState.stack) { Log.d(TAG, "--> " + doc); } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java b/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java index a9929deb9adf..15ad061ac66f 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java +++ b/packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java @@ -16,10 +16,10 @@ package com.android.documentsui; -import com.android.documentsui.model.Document; +import com.android.documentsui.model.DocumentInfo; import com.android.internal.util.Predicate; -public class MimePredicate implements Predicate<Document> { +public class MimePredicate implements Predicate<DocumentInfo> { private final String[] mFilters; public MimePredicate(String[] filters) { @@ -27,7 +27,7 @@ public class MimePredicate implements Predicate<Document> { } @Override - public boolean apply(Document doc) { + public boolean apply(DocumentInfo doc) { if (doc.isDirectory()) { return true; } diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java new file mode 100644 index 000000000000..756a29796039 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import static com.android.documentsui.DocumentsActivity.TAG; + +import android.content.AsyncTaskLoader; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.database.MergeCursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Root; +import android.util.Log; + +import com.android.documentsui.DocumentsActivity.State; +import com.android.documentsui.model.RootInfo; +import com.google.android.collect.Maps; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.AbstractFuture; + +import libcore.io.IoUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class RecentLoader extends AsyncTaskLoader<DirectoryResult> { + + public static final int MAX_OUTSTANDING_RECENTS = 2; + + /** + * Time to wait for first pass to complete before returning partial results. + */ + public static final int MAX_FIRST_PASS_WAIT_MILLIS = 500; + + /** + * Maximum documents from a single root. + */ + public static final int MAX_DOCS_FROM_ROOT = 24; + + private static final ExecutorService sExecutor = buildExecutor(); + + /** + * Create a bounded thread pool for fetching recents; it creates threads as + * needed (up to maximum) and reclaims them when finished. + */ + private static ExecutorService buildExecutor() { + // Create a bounded thread pool for fetching recents; it creates + // threads as needed (up to maximum) and reclaims them when finished. + final ThreadPoolExecutor executor = new ThreadPoolExecutor( + MAX_OUTSTANDING_RECENTS, MAX_OUTSTANDING_RECENTS, 10, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>()); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + private final List<RootInfo> mRoots; + + private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap(); + + private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED; + + private CountDownLatch mFirstPassLatch; + private volatile boolean mFirstPassDone; + + private DirectoryResult mResult; + + // TODO: create better transfer of ownership around cursor to ensure its + // closed in all edge cases. + + public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable { + public final String authority; + public final String rootId; + + private Cursor mWithRoot; + + public RecentTask(String authority, String rootId) { + this.authority = authority; + this.rootId = rootId; + } + + @Override + public void run() { + if (isCancelled()) return; + + final ContentResolver resolver = getContext().getContentResolver(); + final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( + authority); + try { + final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId); + final Cursor cursor = client.query( + uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder)); + mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT); + set(mWithRoot); + + mFirstPassLatch.countDown(); + if (mFirstPassDone) { + onContentChanged(); + } + + } catch (Exception e) { + setException(e); + } finally { + ContentProviderClient.closeQuietly(client); + } + } + + @Override + public void close() throws IOException { + IoUtils.closeQuietly(mWithRoot); + } + } + + public RecentLoader(Context context, List<RootInfo> roots) { + super(context); + mRoots = roots; + } + + @Override + public DirectoryResult loadInBackground() { + if (mFirstPassLatch == null) { + // First time through we kick off all the recent tasks, and wait + // around to see if everyone finishes quickly. + + for (RootInfo root : mRoots) { + if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) { + final RecentTask task = new RecentTask(root.authority, root.rootId); + mTasks.put(root, task); + } + } + + mFirstPassLatch = new CountDownLatch(mTasks.size()); + for (RecentTask task : mTasks.values()) { + sExecutor.execute(task); + } + + try { + mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS); + mFirstPassDone = true; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + // Collect all finished tasks + List<Cursor> cursors = Lists.newArrayList(); + for (RecentTask task : mTasks.values()) { + if (task.isDone()) { + try { + cursors.add(task.get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + Log.w(TAG, "Failed to load " + task.authority + ", " + task.rootId, e); + } + } + } + + final DirectoryResult result = new DirectoryResult(); + if (cursors.size() > 0) { + final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()])); + final SortingCursorWrapper sorted = new SortingCursorWrapper( + merged, State.SORT_ORDER_LAST_MODIFIED) { + @Override + public void close() { + // Ignored, since we manage cursor lifecycle internally + } + }; + result.cursor = sorted; + } + return result; + } + + @Override + public void cancelLoadInBackground() { + super.cancelLoadInBackground(); + } + + @Override + public void deliverResult(DirectoryResult result) { + if (isReset()) { + IoUtils.closeQuietly(result); + return; + } + DirectoryResult oldResult = mResult; + mResult = result; + + if (isStarted()) { + super.deliverResult(result); + } + + if (oldResult != null && oldResult != result) { + IoUtils.closeQuietly(oldResult); + } + } + + @Override + protected void onStartLoading() { + if (mResult != null) { + deliverResult(mResult); + } + if (takeContentChanged() || mResult == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void onCanceled(DirectoryResult result) { + IoUtils.closeQuietly(result); + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + for (RecentTask task : mTasks.values()) { + IoUtils.closeQuietly(task); + } + + IoUtils.closeQuietly(mResult); + mResult = null; + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java index 3447a51aa784..fd7293d46b87 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java @@ -29,7 +29,6 @@ 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; @@ -43,11 +42,14 @@ import android.widget.ListView; import android.widget.TextView; import com.android.documentsui.model.DocumentStack; +import com.android.documentsui.model.RootInfo; import com.google.android.collect.Lists; import libcore.io.IoUtils; -import java.io.FileNotFoundException; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -138,12 +140,13 @@ public class RecentsCreateFragment extends Fragment { uri, null, null, null, RecentsProvider.COL_TIMESTAMP + " DESC", signal); try { while (cursor != null && cursor.moveToNext()) { - final String rawStack = cursor.getString( + final byte[] raw = cursor.getBlob( cursor.getColumnIndex(RecentsProvider.COL_PATH)); try { - final DocumentStack stack = DocumentStack.deserialize(resolver, rawStack); + final DocumentStack stack = new DocumentStack(); + stack.read(new DataInputStream(new ByteArrayInputStream(raw))); result.add(stack); - } catch (FileNotFoundException e) { + } catch (IOException e) { Log.w(TAG, "Failed to resolve stack: " + e); } } @@ -181,7 +184,7 @@ public class RecentsCreateFragment extends Fragment { final View summaryList = convertView.findViewById(R.id.summary_list); final DocumentStack stack = getItem(position); - final DocumentRoot root = stack.getRoot(roots); + final RootInfo root = stack.getRoot(roots); icon.setImageDrawable(root.loadIcon(context)); final StringBuilder builder = new StringBuilder(); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java index dbcb0396fdc4..0c87783673a9 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java @@ -61,6 +61,7 @@ public class RecentsProvider extends ContentProvider { public static final String COL_PACKAGE_NAME = "package_name"; public static final String COL_TIMESTAMP = "timestamp"; + @Deprecated public static Uri buildRecentOpen() { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY).appendPath("recent_open").build(); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java new file mode 100644 index 000000000000..d0e5ff6312ff --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import android.database.AbstractCursor; +import android.database.Cursor; + +/** + * Cursor wrapper that adds columns to identify which root a document came from. + */ +public class RootCursorWrapper extends AbstractCursor { + private final String mAuthority; + private final String mRootId; + + private final Cursor mCursor; + private final int mCount; + + private final String[] mColumnNames; + + private final int mAuthorityIndex; + private final int mRootIdIndex; + + public static final String COLUMN_AUTHORITY = "android:authority"; + public static final String COLUMN_ROOT_ID = "android:rootId"; + + public RootCursorWrapper(String authority, String rootId, Cursor cursor, int maxCount) { + mAuthority = authority; + mRootId = rootId; + mCursor = cursor; + + final int count = cursor.getCount(); + if (maxCount > 0 && count > maxCount) { + mCount = maxCount; + } else { + mCount = count; + } + + if (cursor.getColumnIndex(COLUMN_AUTHORITY) != -1 + || cursor.getColumnIndex(COLUMN_ROOT_ID) != -1) { + throw new IllegalArgumentException("Cursor contains internal columns!"); + } + final String[] before = cursor.getColumnNames(); + mColumnNames = new String[before.length + 2]; + System.arraycopy(before, 0, mColumnNames, 0, before.length); + mAuthorityIndex = before.length; + mRootIdIndex = before.length + 1; + mColumnNames[mAuthorityIndex] = COLUMN_AUTHORITY; + mColumnNames[mRootIdIndex] = COLUMN_ROOT_ID; + } + + @Override + public void close() { + super.close(); + mCursor.close(); + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + return mCursor.moveToPosition(newPosition); + } + + @Override + public String[] getColumnNames() { + return mColumnNames; + } + + @Override + public int getCount() { + return mCount; + } + + @Override + public double getDouble(int column) { + return mCursor.getDouble(column); + } + + @Override + public float getFloat(int column) { + return mCursor.getFloat(column); + } + + @Override + public int getInt(int column) { + return mCursor.getInt(column); + } + + @Override + public long getLong(int column) { + return mCursor.getLong(column); + } + + @Override + public short getShort(int column) { + return mCursor.getShort(column); + } + + @Override + public String getString(int column) { + if (column == mAuthorityIndex) { + return mAuthority; + } else if (column == mRootIdIndex) { + return mRootId; + } else { + return mCursor.getString(column); + } + } + + @Override + public int getType(int column) { + return mCursor.getType(column); + } + + @Override + public boolean isNull(int column) { + return mCursor.isNull(column); + } + +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java index aa21457e74d5..0b10f197f82b 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java @@ -25,17 +25,23 @@ 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.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; import android.util.Log; +import com.android.documentsui.DocumentsActivity.State; +import com.android.documentsui.model.RootInfo; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Objects; import com.google.android.collect.Lists; +import libcore.io.IoUtils; + +import java.util.ArrayList; import java.util.List; /** @@ -46,11 +52,13 @@ public class RootsCache { // TODO: cache roots in local provider to avoid spinning up backends // TODO: root updates should trigger UI refresh + private static final boolean RECENTS_ENABLED = true; + private final Context mContext; - public List<DocumentRoot> mRoots = Lists.newArrayList(); + public List<RootInfo> mRoots = Lists.newArrayList(); - private DocumentRoot mRecentsRoot; + private RootInfo mRecentsRoot; public RootsCache(Context context) { mContext = context; @@ -64,14 +72,13 @@ public class RootsCache { public void update() { mRoots.clear(); - { + if (RECENTS_ENABLED) { // Create special root for recents - final DocumentRoot root = new DocumentRoot(); - root.rootType = DocumentRoot.ROOT_TYPE_SHORTCUT; - root.docId = null; + final RootInfo root = new RootInfo(); + root.rootType = Root.ROOT_TYPE_SHORTCUT; root.icon = R.drawable.ic_dir; + root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE; root.title = mContext.getString(R.string.root_recent); - root.summary = null; root.availableBytes = -1; mRoots.add(root); @@ -89,28 +96,32 @@ public class RootsCache { // TODO: remove deprecated customRoots flag // TODO: populate roots on background thread, and cache results + final Uri rootsUri = DocumentsContract.buildRootsUri(info.authority); final ContentProviderClient client = resolver .acquireUnstableContentProviderClient(info.authority); + Cursor cursor = null; try { - final List<DocumentRoot> roots = DocumentsContract.getDocumentRoots(client); - for (DocumentRoot root : roots) { - root.authority = info.authority; + cursor = client.query(rootsUri, null, null, null, null); + while (cursor.moveToNext()) { + final RootInfo root = RootInfo.fromRootsCursor(info.authority, cursor); + mRoots.add(root); } - mRoots.addAll(roots); } catch (Exception e) { Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e); } finally { + IoUtils.closeQuietly(cursor); ContentProviderClient.closeQuietly(client); } } } } - public DocumentRoot findRoot(Uri uri) { + @Deprecated + public RootInfo 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)) { + final String docId = DocumentsContract.getDocumentId(uri); + for (RootInfo root : mRoots) { + if (Objects.equal(root.authority, authority) && Objects.equal(root.documentId, docId)) { return root; } } @@ -118,23 +129,87 @@ public class RootsCache { } @GuardedBy("ActivityThread") - public DocumentRoot getRecentsRoot() { + public RootInfo getRoot(String authority, String rootId) { + for (RootInfo root : mRoots) { + if (Objects.equal(root.authority, authority) && Objects.equal(root.rootId, rootId)) { + return root; + } + } + return null; + } + + @GuardedBy("ActivityThread") + public RootInfo getRecentsRoot() { return mRecentsRoot; } @GuardedBy("ActivityThread") - public boolean isRecentsRoot(DocumentRoot root) { + public boolean isRecentsRoot(RootInfo root) { return mRecentsRoot == root; } @GuardedBy("ActivityThread") - public List<DocumentRoot> getRoots() { + public List<RootInfo> getRoots() { return mRoots; } + /** + * Flags that declare explicit content types. + */ + private static final int FLAGS_CONTENT_MASK = Root.FLAG_PROVIDES_IMAGES + | Root.FLAG_PROVIDES_AUDIO | Root.FLAG_PROVIDES_VIDEO; + + @GuardedBy("ActivityThread") + public List<RootInfo> getMatchingRoots(State state) { + + // Determine acceptable content flags + int includeFlags = 0; + for (String acceptMime : state.acceptMimes) { + final String[] type = acceptMime.split("/"); + if (type.length != 2) continue; + + if ("image".equals(type[0])) { + includeFlags |= Root.FLAG_PROVIDES_IMAGES; + } else if ("audio".equals(type[0])) { + includeFlags |= Root.FLAG_PROVIDES_AUDIO; + } else if ("video".equals(type[0])) { + includeFlags |= Root.FLAG_PROVIDES_VIDEO; + } else if ("*".equals(type[0])) { + includeFlags |= Root.FLAG_PROVIDES_IMAGES | Root.FLAG_PROVIDES_AUDIO + | Root.FLAG_PROVIDES_VIDEO; + } + } + + ArrayList<RootInfo> matching = Lists.newArrayList(); + for (RootInfo root : mRoots) { + final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0; + final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0; + final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0; + + // Exclude read-only devices when creating + if (state.action == State.ACTION_CREATE && !supportsCreate) continue; + // Exclude advanced devices when not requested + if (!state.showAdvanced && advanced) continue; + // Exclude non-local devices when local only + if (state.localOnly && !localOnly) continue; + + if ((root.flags & FLAGS_CONTENT_MASK) != 0) { + // This root offers specific content, so only include if the + // caller asked for that content type. + if ((root.flags & includeFlags) == 0) { + // Sorry, no overlap. + continue; + } + } + + matching.add(root); + } + return matching; + } + @GuardedBy("ActivityThread") public static Drawable resolveDocumentIcon(Context context, String mimeType) { - if (Documents.MIME_TYPE_DIR.equals(mimeType)) { + if (Document.MIME_TYPE_DIR.equals(mimeType)) { return context.getResources().getDrawable(R.drawable.ic_dir); } else { final PackageManager pm = context.getPackageManager(); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java index 2cfa84198950..ef3a31d86ee9 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java @@ -16,8 +16,6 @@ package com.android.documentsui; -import static com.android.documentsui.DocumentsActivity.TAG; - import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; @@ -26,9 +24,8 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.provider.DocumentsContract.DocumentRoot; +import android.provider.DocumentsContract.Root; import android.text.format.Formatter; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -39,8 +36,10 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; +import com.android.documentsui.DocumentsActivity.State; import com.android.documentsui.SectionedListAdapter.SectionAdapter; -import com.android.documentsui.model.Document; +import com.android.documentsui.model.DocumentInfo; +import com.android.documentsui.model.RootInfo; import java.util.Comparator; import java.util.List; @@ -75,24 +74,31 @@ public class RootsFragment extends Fragment { public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final Context context = inflater.getContext(); - final RootsCache roots = DocumentsApplication.getRootsCache(context); final View view = inflater.inflate(R.layout.fragment_roots, container, false); mList = (ListView) view.findViewById(android.R.id.list); mList.setOnItemClickListener(mItemListener); - final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS); - mAdapter = new SectionedRootsAdapter(context, roots.getRoots(), includeApps); - return view; } @Override public void onStart() { super.onStart(); + updateRootsAdapter(); + } + private void updateRootsAdapter() { final Context context = getActivity(); - mAdapter.updateVisible(SettingsActivity.getDisplayAdvancedDevices(context)); + + final State state = ((DocumentsActivity) context).getDisplayState(); + state.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(context); + + final RootsCache roots = DocumentsApplication.getRootsCache(context); + final List<RootInfo> matchingRoots = roots.getMatchingRoots(state); + final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS); + + mAdapter = new SectionedRootsAdapter(context, matchingRoots, includeApps); mList.setAdapter(mAdapter); } @@ -101,8 +107,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 DocumentRoot) { - activity.onRootPicked((DocumentRoot) item, true); + if (item instanceof RootInfo) { + activity.onRootPicked((RootInfo) item, true); } else if (item instanceof ResolveInfo) { activity.onAppPicked((ResolveInfo) item); } else { @@ -111,7 +117,7 @@ public class RootsFragment extends Fragment { } }; - private static class RootsAdapter extends ArrayAdapter<DocumentRoot> implements SectionAdapter { + private static class RootsAdapter extends ArrayAdapter<RootInfo> implements SectionAdapter { private int mHeaderId; public RootsAdapter(Context context, int headerId) { @@ -131,15 +137,13 @@ 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 DocumentRoot root = getItem(position); + final RootInfo root = getItem(position); icon.setImageDrawable(root.loadIcon(context)); title.setText(root.title); // Device summary is always available space final String summaryText; - if ((root.rootType == DocumentRoot.ROOT_TYPE_DEVICE - || root.rootType == DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED) - && root.availableBytes >= 0) { + if (root.rootType == Root.ROOT_TYPE_DEVICE && root.availableBytes >= 0) { summaryText = context.getString(R.string.root_available_bytes, Formatter.formatFileSize(context, root.availableBytes)); } else { @@ -212,31 +216,24 @@ public class RootsFragment extends Fragment { private final RootsAdapter mServices; private final RootsAdapter mShortcuts; private final RootsAdapter mDevices; - private final RootsAdapter mDevicesAdvanced; private final AppsAdapter mApps; - public SectionedRootsAdapter(Context context, List<DocumentRoot> roots, Intent includeApps) { + public SectionedRootsAdapter(Context context, List<RootInfo> 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 (DocumentRoot root : roots) { - Log.d(TAG, "Found rootType=" + root.rootType); + for (RootInfo root : roots) { switch (root.rootType) { - case DocumentRoot.ROOT_TYPE_SERVICE: + case Root.ROOT_TYPE_SERVICE: mServices.add(root); break; - case DocumentRoot.ROOT_TYPE_SHORTCUT: + case Root.ROOT_TYPE_SHORTCUT: mShortcuts.add(root); break; - case DocumentRoot.ROOT_TYPE_DEVICE: + case Root.ROOT_TYPE_DEVICE: mDevices.add(root); - mDevicesAdvanced.add(root); - break; - case DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED: - mDevicesAdvanced.add(root); break; } } @@ -258,37 +255,36 @@ public class RootsFragment extends Fragment { mServices.sort(comp); mShortcuts.sort(comp); mDevices.sort(comp); - mDevicesAdvanced.sort(comp); - } - public void updateVisible(boolean showAdvanced) { - clearSections(); if (mServices.getCount() > 0) { addSection(mServices); } if (mShortcuts.getCount() > 0) { addSection(mShortcuts); } - - final RootsAdapter devices = showAdvanced ? mDevicesAdvanced : mDevices; - if (devices.getCount() > 0) { - addSection(devices); + if (mDevices.getCount() > 0) { + addSection(mDevices); } - if (mApps.getCount() > 0) { addSection(mApps); } } } - public static class RootComparator implements Comparator<DocumentRoot> { + public static class RootComparator implements Comparator<RootInfo> { @Override - public int compare(DocumentRoot lhs, DocumentRoot rhs) { - final int score = Document.compareToIgnoreCaseNullable(lhs.title, rhs.title); + public int compare(RootInfo lhs, RootInfo rhs) { + if (lhs.authority == null) { + return -1; + } else if (rhs.authority == null) { + return 1; + } + + final int score = DocumentInfo.compareToIgnoreCaseNullable(lhs.title, rhs.title); if (score != 0) { return score; } else { - return Document.compareToIgnoreCaseNullable(lhs.summary, rhs.summary); + return DocumentInfo.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 7e1a29710ee1..8b0a97489a1e 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java @@ -31,7 +31,7 @@ import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; -import com.android.documentsui.model.Document; +import com.android.documentsui.model.DocumentInfo; /** * Display document title editor and save button. @@ -39,7 +39,7 @@ import com.android.documentsui.model.Document; public class SaveFragment extends Fragment { public static final String TAG = "SaveFragment"; - private Document mReplaceTarget; + private DocumentInfo mReplaceTarget; private EditText mDisplayName; private Button mSave; private boolean mIgnoreNextEdit; @@ -128,7 +128,7 @@ public class SaveFragment extends Fragment { * without changing the filename. Can be set to {@code null} if user * navigates outside the target directory. */ - public void setReplaceTarget(Document replaceTarget) { + public void setReplaceTarget(DocumentInfo replaceTarget) { mReplaceTarget = replaceTarget; if (mReplaceTarget != null) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java new file mode 100644 index 000000000000..b434a35c7f2c --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME; +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED; +import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE; + +import android.database.AbstractCursor; +import android.database.Cursor; +import android.provider.DocumentsContract.Document; + +/** + * Cursor wrapper that presents a sorted view of the underlying cursor. Handles + * common {@link Document} sorting modes, such as ordering directories first. + */ +public class SortingCursorWrapper extends AbstractCursor { + private final Cursor mCursor; + + private final int[] mPosition; + private final String[] mValueString; + private final long[] mValueLong; + + public SortingCursorWrapper(Cursor cursor, int sortOrder) { + mCursor = cursor; + + final int count = cursor.getCount(); + mPosition = new int[count]; + switch (sortOrder) { + case SORT_ORDER_DISPLAY_NAME: + mValueString = new String[count]; + mValueLong = null; + break; + case SORT_ORDER_LAST_MODIFIED: + case SORT_ORDER_SIZE: + mValueString = null; + mValueLong = new long[count]; + break; + default: + throw new IllegalArgumentException(); + } + + cursor.moveToPosition(-1); + for (int i = 0; i < count; i++) { + cursor.moveToNext(); + mPosition[i] = i; + + switch (sortOrder) { + case SORT_ORDER_DISPLAY_NAME: + final String mimeType = cursor.getString( + cursor.getColumnIndex(Document.COLUMN_MIME_TYPE)); + final String displayName = cursor.getString( + cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME)); + if (Document.MIME_TYPE_DIR.equals(mimeType)) { + mValueString[i] = '\001' + displayName; + } else { + mValueString[i] = displayName; + } + break; + case SORT_ORDER_LAST_MODIFIED: + mValueLong[i] = cursor.getLong( + cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED)); + break; + case SORT_ORDER_SIZE: + mValueLong[i] = cursor.getLong(cursor.getColumnIndex(Document.COLUMN_SIZE)); + break; + } + } + + switch (sortOrder) { + case SORT_ORDER_DISPLAY_NAME: + synchronized (SortingCursorWrapper.class) { + + binarySort(mPosition, mValueString); + } + break; + case SORT_ORDER_LAST_MODIFIED: + case SORT_ORDER_SIZE: + binarySort(mPosition, mValueLong); + break; + } + } + + @Override + public void close() { + super.close(); + mCursor.close(); + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + return mCursor.moveToPosition(mPosition[newPosition]); + } + + @Override + public String[] getColumnNames() { + return mCursor.getColumnNames(); + } + + @Override + public int getCount() { + return mCursor.getCount(); + } + + @Override + public double getDouble(int column) { + return mCursor.getDouble(column); + } + + @Override + public float getFloat(int column) { + return mCursor.getFloat(column); + } + + @Override + public int getInt(int column) { + return mCursor.getInt(column); + } + + @Override + public long getLong(int column) { + return mCursor.getLong(column); + } + + @Override + public short getShort(int column) { + return mCursor.getShort(column); + } + + @Override + public String getString(int column) { + return mCursor.getString(column); + } + + @Override + public int getType(int column) { + return mCursor.getType(column); + } + + @Override + public boolean isNull(int column) { + return mCursor.isNull(column); + } + + /** + * Borrowed from TimSort.binarySort(), but modified to sort two column + * dataset. + */ + private static void binarySort(int[] position, String[] value) { + final int count = position.length; + for (int start = 1; start < count; start++) { + final int pivotPosition = position[start]; + final String pivotValue = value[start]; + + int left = 0; + int right = start; + + while (left < right) { + int mid = (left + right) >>> 1; + + final String lhs = pivotValue; + final String rhs = value[mid]; + final int compare; + if (lhs == null) { + compare = -1; + } else if (rhs == null) { + compare = 1; + } else { + compare = lhs.compareToIgnoreCase(rhs); + } + + if (compare < 0) { + right = mid; + } else { + left = mid + 1; + } + } + + int n = start - left; + switch (n) { + case 2: + position[left + 2] = position[left + 1]; + value[left + 2] = value[left + 1]; + case 1: + position[left + 1] = position[left]; + value[left + 1] = value[left]; + break; + default: + System.arraycopy(position, left, position, left + 1, n); + System.arraycopy(value, left, value, left + 1, n); + } + + position[left] = pivotPosition; + value[left] = pivotValue; + } + } + + /** + * Borrowed from TimSort.binarySort(), but modified to sort two column + * dataset. + */ + private static void binarySort(int[] position, long[] value) { + final int count = position.length; + for (int start = 1; start < count; start++) { + final int pivotPosition = position[start]; + final long pivotValue = value[start]; + + int left = 0; + int right = start; + + while (left < right) { + int mid = (left + right) >>> 1; + + final long lhs = pivotValue; + final long rhs = value[mid]; + final int compare = Long.compare(lhs, rhs); + if (compare > 0) { + right = mid; + } else { + left = mid + 1; + } + } + + int n = start - left; + switch (n) { + case 2: + position[left + 2] = position[left + 1]; + value[left + 2] = value[left + 1]; + case 1: + position[left + 1] = position[left]; + value[left + 1] = value[left]; + break; + default: + System.arraycopy(position, left, position, left + 1, n); + System.arraycopy(value, left, value, left + 1, n); + } + + position[left] = pivotPosition; + value[left] = pivotValue; + } + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java index f6548e860fc5..2405cb576977 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java @@ -32,7 +32,6 @@ import android.widget.TextView; import libcore.io.IoUtils; import libcore.io.Streams; -import java.io.IOException; import java.io.InputStream; public class TestActivity extends Activity { @@ -50,8 +49,11 @@ public class TestActivity extends Activity { view.setOrientation(LinearLayout.VERTICAL); final CheckBox multiple = new CheckBox(context); - multiple.setText("ALLOW_MULTIPLE"); + multiple.setText("\nALLOW_MULTIPLE\n"); view.addView(multiple); + final CheckBox localOnly = new CheckBox(context); + localOnly.setText("\nLOCAL_ONLY\n"); + view.addView(localOnly); Button button; button = new Button(context); @@ -65,6 +67,9 @@ public class TestActivity extends Activity { if (multiple.isChecked()) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } + if (localOnly.isChecked()) { + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + } startActivityForResult(intent, 42); } }); @@ -81,6 +86,28 @@ public class TestActivity extends Activity { if (multiple.isChecked()) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } + if (localOnly.isChecked()) { + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + } + startActivityForResult(intent, 42); + } + }); + view.addView(button); + + button = new Button(context); + button.setText("OPEN_DOC audio/ogg"); + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("audio/ogg"); + if (multiple.isChecked()) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } + if (localOnly.isChecked()) { + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + } startActivityForResult(intent, 42); } }); @@ -99,6 +126,9 @@ public class TestActivity extends Activity { if (multiple.isChecked()) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } + if (localOnly.isChecked()) { + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + } startActivityForResult(intent, 42); } }); @@ -113,6 +143,9 @@ public class TestActivity extends Activity { intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TITLE, "foobar.txt"); + if (localOnly.isChecked()) { + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + } startActivityForResult(intent, 42); } }); @@ -129,6 +162,9 @@ public class TestActivity extends Activity { if (multiple.isChecked()) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } + if (localOnly.isChecked()) { + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + } startActivityForResult(Intent.createChooser(intent, "Kittens!"), 42); } }); diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java deleted file mode 100644 index 692d17145756..000000000000 --- a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java +++ /dev/null @@ -1,217 +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.ContentResolver; -import android.database.Cursor; -import android.net.Uri; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.DocumentColumns; -import android.provider.DocumentsContract.Documents; - -import com.android.documentsui.RecentsProvider; - -import libcore.io.IoUtils; - -import java.io.FileNotFoundException; -import java.util.Comparator; - -/** - * Representation of a single document. - */ -public class Document { - public final Uri uri; - public final String mimeType; - public final String displayName; - public final long lastModified; - public final int flags; - public final String summary; - public final long size; - - private Document(Uri uri, String mimeType, String displayName, long lastModified, int flags, - String summary, long size) { - this.uri = uri; - this.mimeType = mimeType; - this.displayName = displayName; - this.lastModified = lastModified; - this.flags = flags; - this.summary = summary; - this.size = size; - } - - public static Document fromDirectoryCursor(Uri parent, Cursor cursor) { - final String authority = parent.getAuthority(); - final String docId = getCursorString(cursor, DocumentColumns.DOC_ID); - - 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); - final int flags = getCursorInt(cursor, DocumentColumns.FLAGS); - final String summary = getCursorString(cursor, DocumentColumns.SUMMARY); - final long size = getCursorLong(cursor, DocumentColumns.SIZE); - - 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)); - final long lastModified = getCursorLong(recentCursor, RecentsProvider.COL_TIMESTAMP); - - Cursor cursor = null; - try { - cursor = resolver.query(uri, null, null, null, null); - if (!cursor.moveToFirst()) { - throw new FileNotFoundException("Missing details for " + uri); - } - final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE); - final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME); - final int flags = getCursorInt(cursor, DocumentColumns.FLAGS) - & Documents.FLAG_SUPPORTS_THUMBNAIL; - final String summary = getCursorString(cursor, DocumentColumns.SUMMARY); - final long size = getCursorLong(cursor, DocumentColumns.SIZE); - - return new Document(uri, mimeType, displayName, lastModified, flags, summary, size); - } catch (Throwable t) { - throw asFileNotFoundException(t); - } finally { - IoUtils.closeQuietly(cursor); - } - } - - public static Document fromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException { - Cursor cursor = null; - try { - cursor = resolver.query(uri, null, null, null, null); - if (!cursor.moveToFirst()) { - throw new FileNotFoundException("Missing details for " + uri); - } - final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE); - final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME); - final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED); - final int flags = getCursorInt(cursor, DocumentColumns.FLAGS); - final String summary = getCursorString(cursor, DocumentColumns.SUMMARY); - final long size = getCursorLong(cursor, DocumentColumns.SIZE); - - return new Document(uri, mimeType, displayName, lastModified, flags, summary, size); - } catch (Throwable t) { - throw asFileNotFoundException(t); - } finally { - IoUtils.closeQuietly(cursor); - } - } - - @Override - public String toString() { - return "Document{name=" + displayName + ", uri=" + uri + "}"; - } - - public boolean isCreateSupported() { - return (flags & Documents.FLAG_SUPPORTS_CREATE) != 0; - } - - public boolean isSearchSupported() { - return (flags & Documents.FLAG_SUPPORTS_SEARCH) != 0; - } - - public boolean isThumbnailSupported() { - return (flags & Documents.FLAG_SUPPORTS_THUMBNAIL) != 0; - } - - public boolean isDirectory() { - return Documents.MIME_TYPE_DIR.equals(mimeType); - } - - public boolean isGridPreferred() { - return (flags & Documents.FLAG_PREFERS_GRID) != 0; - } - - public boolean isDeleteSupported() { - return (flags & Documents.FLAG_SUPPORTS_DELETE) != 0; - } - - private static String getCursorString(Cursor cursor, String columnName) { - final int index = cursor.getColumnIndex(columnName); - return (index != -1) ? cursor.getString(index) : null; - } - - /** - * Missing or null values are returned as -1. - */ - private static long getCursorLong(Cursor cursor, String columnName) { - final int index = cursor.getColumnIndex(columnName); - if (index == -1) return -1; - final String value = cursor.getString(index); - if (value == null) return -1; - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return -1; - } - } - - private static int getCursorInt(Cursor cursor, String columnName) { - final int index = cursor.getColumnIndex(columnName); - return (index != -1) ? cursor.getInt(index) : 0; - } - - public static class DisplayNameComparator implements Comparator<Document> { - @Override - public int compare(Document lhs, Document rhs) { - final boolean leftDir = lhs.isDirectory(); - final boolean rightDir = rhs.isDirectory(); - - if (leftDir != rightDir) { - return leftDir ? -1 : 1; - } else { - return compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName); - } - } - } - - public static class LastModifiedComparator implements Comparator<Document> { - @Override - public int compare(Document lhs, Document rhs) { - return Long.compare(rhs.lastModified, lhs.lastModified); - } - } - - public static class SizeComparator implements Comparator<Document> { - @Override - public int compare(Document lhs, Document rhs) { - return Long.compare(rhs.size, lhs.size); - } - } - - public static FileNotFoundException asFileNotFoundException(Throwable t) - throws FileNotFoundException { - if (t instanceof FileNotFoundException) { - throw (FileNotFoundException) t; - } - final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage()); - 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/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java new file mode 100644 index 000000000000..7721bcc7bcdb --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.model; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; + +import com.android.documentsui.RecentsProvider; +import com.android.documentsui.RootCursorWrapper; + +import libcore.io.IoUtils; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.ProtocolException; +import java.util.Comparator; + +/** + * Representation of a {@link Document}. + */ +public class DocumentInfo implements Durable { + private static final int VERSION_INIT = 1; + + public Uri uri; + public String mimeType; + public String displayName; + public long lastModified; + public int flags; + public String summary; + public long size; + public int icon; + + public DocumentInfo() { + reset(); + } + + @Override + public void reset() { + uri = null; + mimeType = null; + displayName = null; + lastModified = -1; + flags = 0; + summary = null; + size = -1; + icon = 0; + } + + @Override + public void read(DataInputStream in) throws IOException { + final int version = in.readInt(); + switch (version) { + case VERSION_INIT: + final String rawUri = DurableUtils.readNullableString(in); + uri = rawUri != null ? Uri.parse(rawUri) : null; + mimeType = DurableUtils.readNullableString(in); + displayName = DurableUtils.readNullableString(in); + lastModified = in.readLong(); + flags = in.readInt(); + summary = DurableUtils.readNullableString(in); + size = in.readLong(); + icon = in.readInt(); + break; + default: + throw new ProtocolException("Unknown version " + version); + } + } + + @Override + public void write(DataOutputStream out) throws IOException { + out.writeInt(VERSION_INIT); + DurableUtils.writeNullableString(out, uri.toString()); + DurableUtils.writeNullableString(out, mimeType); + DurableUtils.writeNullableString(out, displayName); + out.writeLong(lastModified); + out.writeInt(flags); + DurableUtils.writeNullableString(out, summary); + out.writeLong(size); + out.writeInt(icon); + } + + public static DocumentInfo fromDirectoryCursor(Cursor cursor) { + final DocumentInfo doc = new DocumentInfo(); + final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); + final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); + doc.uri = DocumentsContract.buildDocumentUri(authority, docId); + doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); + doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); + doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS); + doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY); + doc.size = getCursorLong(cursor, Document.COLUMN_SIZE); + doc.icon = getCursorInt(cursor, Document.COLUMN_ICON); + return doc; + } + + @Deprecated + public static DocumentInfo fromRecentOpenCursor(ContentResolver resolver, Cursor recentCursor) + throws FileNotFoundException { + final Uri uri = Uri.parse(getCursorString(recentCursor, RecentsProvider.COL_URI)); + final long lastModified = getCursorLong(recentCursor, RecentsProvider.COL_TIMESTAMP); + + Cursor cursor = null; + try { + cursor = resolver.query(uri, null, null, null, null); + if (!cursor.moveToFirst()) { + throw new FileNotFoundException("Missing details for " + uri); + } + + final DocumentInfo doc = new DocumentInfo(); + doc.uri = uri; + doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); + doc.lastModified = lastModified; + doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS) + & Document.FLAG_SUPPORTS_THUMBNAIL; + doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY); + doc.size = getCursorLong(cursor, Document.COLUMN_SIZE); + doc.icon = getCursorInt(cursor, Document.COLUMN_ICON); + return doc; + } catch (Throwable t) { + throw asFileNotFoundException(t); + } finally { + IoUtils.closeQuietly(cursor); + } + } + + public static DocumentInfo fromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException { + Cursor cursor = null; + try { + cursor = resolver.query(uri, null, null, null, null); + if (!cursor.moveToFirst()) { + throw new FileNotFoundException("Missing details for " + uri); + } + final DocumentInfo doc = new DocumentInfo(); + doc.uri = uri; + doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); + doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); + doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS); + doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY); + doc.size = getCursorLong(cursor, Document.COLUMN_SIZE); + doc.icon = getCursorInt(cursor, Document.COLUMN_ICON); + return doc; + } catch (Throwable t) { + throw asFileNotFoundException(t); + } finally { + IoUtils.closeQuietly(cursor); + } + } + + @Override + public String toString() { + return "Document{name=" + displayName + ", uri=" + uri + "}"; + } + + public boolean isCreateSupported() { + return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0; + } + + public boolean isSearchSupported() { + return (flags & Document.FLAG_DIR_SUPPORTS_SEARCH) != 0; + } + + public boolean isThumbnailSupported() { + return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0; + } + + public boolean isDirectory() { + return Document.MIME_TYPE_DIR.equals(mimeType); + } + + public boolean isGridPreferred() { + return (flags & Document.FLAG_DIR_PREFERS_GRID) != 0; + } + + public boolean isDeleteSupported() { + return (flags & Document.FLAG_SUPPORTS_DELETE) != 0; + } + + public Drawable loadIcon(Context context) { + return loadIcon(context, uri.getAuthority(), icon); + } + + public static Drawable loadIcon(Context context, String authority, int icon) { + 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; + } + + public static String getCursorString(Cursor cursor, String columnName) { + final int index = cursor.getColumnIndex(columnName); + return (index != -1) ? cursor.getString(index) : null; + } + + /** + * Missing or null values are returned as -1. + */ + public static long getCursorLong(Cursor cursor, String columnName) { + final int index = cursor.getColumnIndex(columnName); + if (index == -1) return -1; + final String value = cursor.getString(index); + if (value == null) return -1; + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return -1; + } + } + + public static int getCursorInt(Cursor cursor, String columnName) { + final int index = cursor.getColumnIndex(columnName); + return (index != -1) ? cursor.getInt(index) : 0; + } + + @Deprecated + public static class DisplayNameComparator implements Comparator<DocumentInfo> { + @Override + public int compare(DocumentInfo lhs, DocumentInfo rhs) { + final boolean leftDir = lhs.isDirectory(); + final boolean rightDir = rhs.isDirectory(); + + if (leftDir != rightDir) { + return leftDir ? -1 : 1; + } else { + return compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName); + } + } + } + + @Deprecated + public static class LastModifiedComparator implements Comparator<DocumentInfo> { + @Override + public int compare(DocumentInfo lhs, DocumentInfo rhs) { + return Long.compare(rhs.lastModified, lhs.lastModified); + } + } + + @Deprecated + public static class SizeComparator implements Comparator<DocumentInfo> { + @Override + public int compare(DocumentInfo lhs, DocumentInfo rhs) { + return Long.compare(rhs.size, lhs.size); + } + } + + public static FileNotFoundException asFileNotFoundException(Throwable t) + throws FileNotFoundException { + if (t instanceof FileNotFoundException) { + throw (FileNotFoundException) t; + } + final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage()); + 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 81f75d246ce0..64631ab8cf30 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java +++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java @@ -16,57 +16,22 @@ package com.android.documentsui.model; -import static com.android.documentsui.DocumentsActivity.TAG; -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; - -import java.io.FileNotFoundException; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ProtocolException; import java.util.LinkedList; /** - * Representation of a stack of {@link Document}, usually the result of a + * Representation of a stack of {@link DocumentInfo}, usually the result of a * user-driven traversal. */ -public class DocumentStack extends LinkedList<Document> { - - public static String serialize(DocumentStack stack) { - final JSONArray json = new JSONArray(); - for (int i = 0; i < stack.size(); i++) { - json.put(stack.get(i).uri); - } - return json.toString(); - } - - public static DocumentStack deserialize(ContentResolver resolver, String raw) - throws FileNotFoundException { - Log.d(TAG, "deserialize: " + raw); +public class DocumentStack extends LinkedList<DocumentInfo> implements Durable { + private static final int VERSION_INIT = 1; - final DocumentStack stack = new DocumentStack(); - try { - final JSONArray json = new JSONArray(raw); - for (int i = 0; i < json.length(); i++) { - final Uri uri = Uri.parse(json.getString(i)); - final Document doc = Document.fromUri(resolver, uri); - stack.add(doc); - } - } catch (JSONException e) { - throw asFileNotFoundException(e); - } - - // TODO: handle roots that have gone missing - return stack; - } - - public DocumentRoot getRoot(RootsCache roots) { + public RootInfo getRoot(RootsCache roots) { return roots.findRoot(getLast().uri); } @@ -79,4 +44,37 @@ public class DocumentStack extends LinkedList<Document> { return null; } } + + @Override + public void reset() { + clear(); + } + + @Override + public void read(DataInputStream in) throws IOException { + final int version = in.readInt(); + switch (version) { + case VERSION_INIT: + final int size = in.readInt(); + for (int i = 0; i < size; i++) { + final DocumentInfo doc = new DocumentInfo(); + doc.read(in); + add(doc); + } + break; + default: + throw new ProtocolException("Unknown version " + version); + } + } + + @Override + public void write(DataOutputStream out) throws IOException { + out.writeInt(VERSION_INIT); + final int size = size(); + out.writeInt(size); + for (int i = 0; i < size; i++) { + final DocumentInfo doc = get(i); + doc.write(out); + } + } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Durable.java b/packages/DocumentsUI/src/com/android/documentsui/model/Durable.java new file mode 100644 index 000000000000..01633edca33f --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/model/Durable.java @@ -0,0 +1,27 @@ +/* + * 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 java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public interface Durable { + public void reset(); + public void read(DataInputStream in) throws IOException; + public void write(DataOutputStream out) throws IOException; +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java b/packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java new file mode 100644 index 000000000000..214fb144e8c9 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java @@ -0,0 +1,100 @@ +/* + * 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 static com.android.documentsui.DocumentsActivity.TAG; + +import android.os.BadParcelableException; +import android.os.Parcel; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class DurableUtils { + public static <D extends Durable> byte[] writeToArray(D d) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + d.write(new DataOutputStream(out)); + return out.toByteArray(); + } + + public static <D extends Durable> D readFromArray(byte[] data, D d) throws IOException { + final ByteArrayInputStream in = new ByteArrayInputStream(data); + d.reset(); + try { + d.read(new DataInputStream(in)); + } catch (IOException e) { + d.reset(); + throw e; + } + return d; + } + + public static <D extends Durable> byte[] writeToArrayOrNull(D d) { + try { + return writeToArray(d); + } catch (IOException e) { + Log.w(TAG, "Failed to write", e); + return null; + } + } + + public static <D extends Durable> D readFromArrayOrNull(byte[] data, D d) { + try { + return readFromArray(data, d); + } catch (IOException e) { + Log.w(TAG, "Failed to read", e); + return null; + } + } + + public static <D extends Durable> void writeToParcel(Parcel parcel, D d) { + try { + parcel.writeByteArray(writeToArray(d)); + } catch (IOException e) { + throw new BadParcelableException(e); + } + } + + public static <D extends Durable> D readFromParcel(Parcel parcel, D d) { + try { + return readFromArray(parcel.createByteArray(), d); + } catch (IOException e) { + throw new BadParcelableException(e); + } + } + + public static void writeNullableString(DataOutputStream out, String value) throws IOException { + if (value != null) { + out.write(1); + out.writeUTF(value); + } else { + out.write(0); + } + } + + public static String readNullableString(DataInputStream in) throws IOException { + if (in.read() != 0) { + return in.readUTF(); + } else { + return null; + } + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java new file mode 100644 index 000000000000..189284b2d97e --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java @@ -0,0 +1,80 @@ +/* + * 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 static com.android.documentsui.model.DocumentInfo.getCursorInt; +import static com.android.documentsui.model.DocumentInfo.getCursorLong; +import static com.android.documentsui.model.DocumentInfo.getCursorString; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.provider.DocumentsContract.Root; + +import java.util.Objects; + +/** + * Representation of a {@link Root}. + */ +public class RootInfo { + public String authority; + public String rootId; + public int rootType; + public int flags; + public int icon; + public String title; + public String summary; + public String documentId; + public long availableBytes; + + public static RootInfo fromRootsCursor(String authority, Cursor cursor) { + final RootInfo root = new RootInfo(); + root.authority = authority; + root.rootId = getCursorString(cursor, Root.COLUMN_ROOT_ID); + root.rootType = getCursorInt(cursor, Root.COLUMN_ROOT_TYPE); + root.flags = getCursorInt(cursor, Root.COLUMN_FLAGS); + root.icon = getCursorInt(cursor, Root.COLUMN_ICON); + root.title = getCursorString(cursor, Root.COLUMN_TITLE); + root.summary = getCursorString(cursor, Root.COLUMN_SUMMARY); + root.documentId = getCursorString(cursor, Root.COLUMN_DOCUMENT_ID); + root.availableBytes = getCursorLong(cursor, Root.COLUMN_AVAILABLE_BYTES); + return root; + } + + public Drawable loadIcon(Context context) { + return DocumentInfo.loadIcon(context, authority, icon); + } + + @Override + public boolean equals(Object o) { + if (o instanceof RootInfo) { + final RootInfo root = (RootInfo) o; + return Objects.equals(authority, root.authority) && Objects.equals(rootId, root.rootId); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(authority, rootId); + } + + public String getDirectoryString() { + return (summary != null) ? summary : title; + } +} diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index 583ecc9e3965..bbe3b455f54c 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -26,9 +26,8 @@ import android.media.ExifInterface; import android.os.CancellationSignal; import android.os.Environment; import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract.DocumentColumns; -import android.provider.DocumentsContract.DocumentRoot; -import android.provider.DocumentsContract.Documents; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.webkit.MimeTypeMap; @@ -41,7 +40,6 @@ 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 DocumentsProvider { @@ -49,36 +47,52 @@ public class ExternalStorageProvider extends DocumentsProvider { // docId format: root:path/to/file - 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 static final String[] DEFAULT_ROOT_PROJECTION = new String[] { + Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON, + Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_AVAILABLE_BYTES, }; - private ArrayList<DocumentRoot> mRoots; - private HashMap<String, DocumentRoot> mTagToRoot; - private HashMap<String, File> mTagToPath; + private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { + Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, + }; + + private static class RootInfo { + public String rootId; + public int rootType; + public int flags; + public int icon; + public String title; + public String docId; + } + + private ArrayList<RootInfo> mRoots; + private HashMap<String, RootInfo> mIdToRoot; + private HashMap<String, File> mIdToPath; @Override public boolean onCreate() { mRoots = Lists.newArrayList(); - mTagToRoot = Maps.newHashMap(); - mTagToPath = Maps.newHashMap(); + mIdToRoot = Maps.newHashMap(); + mIdToPath = Maps.newHashMap(); // TODO: support multiple storage devices try { - final String tag = "primary"; + final String rootId = "primary"; final File path = Environment.getExternalStorageDirectory(); - mTagToPath.put(tag, path); + mIdToPath.put(rootId, 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); + final RootInfo root = new RootInfo(); + root.rootId = "primary"; + root.rootType = Root.ROOT_TYPE_DEVICE; + root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED; root.icon = R.drawable.ic_pdf; - root.flags = DocumentRoot.FLAG_LOCAL_ONLY; + root.title = getContext().getString(R.string.root_internal_storage); + root.docId = getDocIdForFile(path); mRoots.add(root); - mTagToRoot.put(tag, root); + mIdToRoot.put(rootId, root); } catch (FileNotFoundException e) { throw new IllegalStateException(e); } @@ -86,12 +100,20 @@ public class ExternalStorageProvider extends DocumentsProvider { return true; } + private static String[] resolveRootProjection(String[] projection) { + return projection != null ? projection : DEFAULT_ROOT_PROJECTION; + } + + private static String[] resolveDocumentProjection(String[] projection) { + return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; + } + private String getDocIdForFile(File file) throws FileNotFoundException { String path = file.getAbsolutePath(); // Find the most-specific root path Map.Entry<String, File> mostSpecific = null; - for (Map.Entry<String, File> root : mTagToPath.entrySet()) { + for (Map.Entry<String, File> root : mIdToPath.entrySet()) { final String rootPath = root.getValue().getPath(); if (path.startsWith(rootPath) && (mostSpecific == null || rootPath.length() > mostSpecific.getValue().getPath().length())) { @@ -121,7 +143,7 @@ public class ExternalStorageProvider extends DocumentsProvider { final String tag = docId.substring(0, splitIndex); final String path = docId.substring(splitIndex + 1); - File target = mTagToPath.get(tag); + File target = mIdToPath.get(tag); if (target == null) { throw new FileNotFoundException("No root for " + tag); } @@ -143,41 +165,48 @@ public class ExternalStorageProvider extends DocumentsProvider { int flags = 0; if (file.isDirectory()) { - flags |= Documents.FLAG_SUPPORTS_SEARCH; + flags |= Document.FLAG_DIR_SUPPORTS_SEARCH; } if (file.isDirectory() && file.canWrite()) { - flags |= Documents.FLAG_SUPPORTS_CREATE; + flags |= Document.FLAG_DIR_SUPPORTS_CREATE; } if (file.canWrite()) { - flags |= Documents.FLAG_SUPPORTS_WRITE; - flags |= Documents.FLAG_SUPPORTS_RENAME; - flags |= Documents.FLAG_SUPPORTS_DELETE; + flags |= Document.FLAG_SUPPORTS_WRITE; + flags |= Document.FLAG_SUPPORTS_DELETE; } final String displayName = file.getName(); final String mimeType = getTypeForFile(file); if (mimeType.startsWith("image/")) { - flags |= Documents.FLAG_SUPPORTS_THUMBNAIL; + flags |= Document.FLAG_SUPPORTS_THUMBNAIL; } final RowBuilder row = result.newRow(); - row.offer(DocumentColumns.DOC_ID, docId); - row.offer(DocumentColumns.DISPLAY_NAME, displayName); - row.offer(DocumentColumns.SIZE, file.length()); - row.offer(DocumentColumns.MIME_TYPE, mimeType); - row.offer(DocumentColumns.LAST_MODIFIED, file.lastModified()); - row.offer(DocumentColumns.FLAGS, flags); + row.offer(Document.COLUMN_DOCUMENT_ID, docId); + row.offer(Document.COLUMN_DISPLAY_NAME, displayName); + row.offer(Document.COLUMN_SIZE, file.length()); + row.offer(Document.COLUMN_MIME_TYPE, mimeType); + row.offer(Document.COLUMN_LAST_MODIFIED, file.lastModified()); + row.offer(Document.COLUMN_FLAGS, flags); } @Override - 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(); + public Cursor queryRoots(String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); + for (String rootId : mIdToPath.keySet()) { + final RootInfo root = mIdToRoot.get(rootId); + final File path = mIdToPath.get(rootId); + + final RowBuilder row = result.newRow(); + row.offer(Root.COLUMN_ROOT_ID, root.rootId); + row.offer(Root.COLUMN_ROOT_TYPE, root.rootType); + row.offer(Root.COLUMN_FLAGS, root.flags); + row.offer(Root.COLUMN_ICON, root.icon); + row.offer(Root.COLUMN_TITLE, root.title); + row.offer(Root.COLUMN_DOCUMENT_ID, root.docId); + row.offer(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace()); } - return mRoots; + return result; } @Override @@ -187,7 +216,7 @@ public class ExternalStorageProvider extends DocumentsProvider { displayName = validateDisplayName(mimeType, displayName); final File file = new File(parent, displayName); - if (Documents.MIME_TYPE_DIR.equals(mimeType)) { + if (Document.MIME_TYPE_DIR.equals(mimeType)) { if (!file.mkdir()) { throw new IllegalStateException("Failed to mkdir " + file); } @@ -204,16 +233,6 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override - public void renameDocument(String docId, String displayName) throws FileNotFoundException { - final File file = getFileForDocId(docId); - final File newFile = new File(file.getParentFile(), displayName); - if (!file.renameTo(newFile)) { - throw new IllegalStateException("Failed to rename " + docId); - } - // TODO: update any outstanding grants - } - - @Override public void deleteDocument(String docId) throws FileNotFoundException { final File file = getFileForDocId(docId); if (!file.delete()) { @@ -222,16 +241,19 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override - public Cursor queryDocument(String docId) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS); - includeFile(result, docId, null); + public Cursor queryDocument(String documentId, String[] projection) + throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + includeFile(result, documentId, null); return result; } @Override - public Cursor queryDocumentChildren(String docId) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS); - final File parent = getFileForDocId(docId); + public Cursor queryChildDocuments( + String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + final File parent = getFileForDocId(parentDocumentId); for (File file : parent.listFiles()) { includeFile(result, null, file); } @@ -239,9 +261,10 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override - public Cursor querySearch(String docId, String query) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS); - final File parent = getFileForDocId(docId); + public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection) + throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + final File parent = getFileForDocId(parentDocumentId); final LinkedList<File> pending = new LinkedList<File>(); pending.add(parent); @@ -261,22 +284,24 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override - public String getType(String docId) throws FileNotFoundException { - final File file = getFileForDocId(docId); + public String getDocumentType(String documentId) throws FileNotFoundException { + final File file = getFileForDocId(documentId); return getTypeForFile(file); } @Override - public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) + public ParcelFileDescriptor openDocument( + String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { - final File file = getFileForDocId(docId); + final File file = getFileForDocId(documentId); return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(null, mode)); } @Override public AssetFileDescriptor openDocumentThumbnail( - String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { - final File file = getFileForDocId(docId); + String documentId, Point sizeHint, CancellationSignal signal) + throws FileNotFoundException { + final File file = getFileForDocId(documentId); final ParcelFileDescriptor pfd = ParcelFileDescriptor.open( file, ParcelFileDescriptor.MODE_READ_ONLY); @@ -294,7 +319,7 @@ public class ExternalStorageProvider extends DocumentsProvider { private static String getTypeForFile(File file) { if (file.isDirectory()) { - return Documents.MIME_TYPE_DIR; + return Document.MIME_TYPE_DIR; } else { return getTypeForName(file.getName()); } @@ -314,7 +339,7 @@ public class ExternalStorageProvider extends DocumentsProvider { } private static String validateDisplayName(String mimeType, String displayName) { - if (Documents.MIME_TYPE_DIR.equals(mimeType)) { + if (Document.MIME_TYPE_DIR.equals(mimeType)) { return displayName; } else { // Try appending meaningful extension if needed |