summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt92
-rw-r--r--core/java/android/provider/DocumentsContract.java756
-rw-r--r--core/java/android/provider/DocumentsProvider.java229
-rw-r--r--packages/DocumentsUI/Android.mk2
-rw-r--r--packages/DocumentsUI/AndroidManifest.xml7
-rw-r--r--packages/DocumentsUI/res/values/strings.xml2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java20
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java288
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java175
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java321
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/MimePredicate.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java255
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java15
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java1
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java132
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsCache.java117
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java80
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java256
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/TestActivity.java40
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/Document.java217
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java294
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java84
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/Durable.java27
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java100
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java80
-rw-r--r--packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java163
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