am 9fd81a90: Merge "Stronger DocumentsProvider contract." into klp-dev

* commit '9fd81a9008d5c8dd33272b6a451d89fa2fa1841e':
  Stronger DocumentsProvider contract.
This commit is contained in:
Jeff Sharkey
2013-08-28 21:32:08 -07:00
committed by Android Git Automerger
23 changed files with 1123 additions and 1339 deletions

View File

@@ -20814,40 +20814,46 @@ package android.provider {
}
public final class DocumentsContract {
ctor public DocumentsContract();
method public static android.net.Uri buildContentsUri(java.lang.String, java.lang.String, java.lang.String);
method public static android.net.Uri buildContentsUri(android.net.Uri);
method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String, java.lang.String);
method public static android.net.Uri buildDocumentUri(android.net.Uri, java.lang.String);
method public static android.net.Uri buildRootUri(java.lang.String, java.lang.String);
method public static android.net.Uri buildRootsUri(java.lang.String);
method public static android.net.Uri buildSearchUri(java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
method public static android.net.Uri createDocument(android.content.ContentResolver, android.net.Uri, java.lang.String, java.lang.String);
method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
method public static java.lang.String getDocId(android.net.Uri);
method public static android.net.Uri[] getOpenDocuments(android.content.Context);
method public static java.lang.String getRootId(android.net.Uri);
method public static java.lang.String getSearchQuery(android.net.Uri);
method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
method public static boolean isLocalOnly(android.net.Uri);
method public static void notifyRootsChanged(android.content.Context, java.lang.String);
method public static boolean renameDocument(android.content.ContentResolver, android.net.Uri, java.lang.String);
method public static android.net.Uri setLocalOnly(android.net.Uri);
field public static final java.lang.String EXTRA_HAS_MORE = "has_more";
field public static final java.lang.String EXTRA_REQUEST_MORE = "request_more";
field public static final java.lang.String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
field public static final java.lang.String EXTRA_ERROR = "error";
field public static final java.lang.String EXTRA_INFO = "info";
field public static final java.lang.String EXTRA_LOADING = "loading";
}
public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
field public static final java.lang.String DOC_ID = "doc_id";
field public static final java.lang.String FLAGS = "flags";
field public static final java.lang.String ICON = "icon";
field public static final java.lang.String LAST_MODIFIED = "last_modified";
field public static final java.lang.String MIME_TYPE = "mime_type";
field public static final java.lang.String SUMMARY = "summary";
}
public static class DocumentsContract.Documents {
field public static final java.lang.String DOC_ID_ROOT = "0";
public static final class DocumentsContract.DocumentRoot implements android.os.Parcelable {
ctor public DocumentsContract.DocumentRoot();
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
field public static final int FLAG_LOCAL_ONLY = 2; // 0x2
field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
field public long availableBytes;
field public java.lang.String docId;
field public int flags;
field public int icon;
field public java.lang.String[] mimeTypes;
field public java.lang.String recentDocId;
field public int rootType;
field public java.lang.String summary;
field public java.lang.String title;
}
public static final class DocumentsContract.Documents {
field public static final int FLAG_PREFERS_GRID = 64; // 0x40
field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4
@@ -20855,25 +20861,32 @@ package android.provider {
field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10
field public static final int FLAG_SUPPORTS_THUMBNAIL = 8; // 0x8
field public static final int FLAG_SUPPORTS_WRITE = 32; // 0x20
field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.doc/dir";
}
public static abstract interface DocumentsContract.RootColumns {
field public static final java.lang.String AVAILABLE_BYTES = "available_bytes";
field public static final java.lang.String ICON = "icon";
field public static final java.lang.String ROOT_ID = "root_id";
field public static final java.lang.String ROOT_TYPE = "root_type";
field public static final java.lang.String SUMMARY = "summary";
field public static final java.lang.String TITLE = "title";
}
public static class DocumentsContract.Roots {
field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
field public static final java.lang.String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
public abstract class DocumentsProvider extends android.content.ContentProvider {
ctor public DocumentsProvider();
method public final android.os.Bundle callFromPackage(java.lang.String, java.lang.String, java.lang.String, android.os.Bundle);
method public java.lang.String createDocument(java.lang.String, java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
method public void deleteDocument(java.lang.String) throws java.io.FileNotFoundException;
method public abstract java.util.List<android.provider.DocumentsContract.DocumentRoot> getDocumentRoots();
method public java.lang.String getType(java.lang.String) throws java.io.FileNotFoundException;
method public final java.lang.String getType(android.net.Uri);
method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
method public void notifyDocumentRootsChanged();
method public abstract android.os.ParcelFileDescriptor openDocument(java.lang.String, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public android.content.res.AssetFileDescriptor openDocumentThumbnail(java.lang.String, android.graphics.Point, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException;
method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String);
method public abstract android.database.Cursor queryDocument(java.lang.String) throws java.io.FileNotFoundException;
method public abstract android.database.Cursor queryDocumentChildren(java.lang.String) throws java.io.FileNotFoundException;
method public android.database.Cursor querySearch(java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
method public void renameDocument(java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
}
public final deprecated class LiveFolders implements android.provider.BaseColumns {

View File

@@ -316,4 +316,11 @@ public class ContentProviderClient {
public ContentProvider getLocalContentProvider() {
return ContentProvider.coerceToLocalContentProvider(mContentProvider);
}
/** {@hide} */
public static void closeQuietly(ContentProviderClient client) {
if (client != null) {
client.release();
}
}
}

View File

@@ -2687,10 +2687,6 @@ public class Intent implements Parcelable, Cloneable {
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
/** {@hide} */
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_MANAGE_DOCUMENT = "android.intent.action.MANAGE_DOCUMENT";
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Standard intent categories (see addCategory()).

View File

@@ -22,6 +22,7 @@ import android.util.SparseArray;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
@@ -545,6 +546,13 @@ public final class Bundle implements Parcelable, Cloneable {
mFdsKnown = false;
}
/** {@hide} */
public void putParcelableList(String key, List<? extends Parcelable> value) {
unparcel();
mMap.put(key, value);
mFdsKnown = false;
}
/**
* Inserts a SparceArray of Parcelable values into the mapping of this
* Bundle, replacing any existing value for the given key. Either key

View File

@@ -19,9 +19,8 @@ package android.provider;
import static android.net.TrafficStats.KB_IN_BYTES;
import static libcore.io.OsConstants.SEEK_SET;
import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -31,12 +30,16 @@ import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.Parcelable;
import android.util.Log;
import com.android.internal.util.Preconditions;
import com.google.android.collect.Lists;
import libcore.io.ErrnoException;
@@ -51,74 +54,49 @@ import java.util.List;
/**
* Defines the contract between a documents provider and the platform.
* <p>
* A document provider is a {@link ContentProvider} that presents a set of
* documents in a hierarchical structure. The system provides UI that visualizes
* all available document providers, offering users the ability to open existing
* documents or create new documents.
* <p>
* Each provider expresses one or more "roots" which each serve as the top-level
* of a tree. For example, a root could represent an account, or a physical
* storage device. Under each root, documents are referenced by a unique
* {@link DocumentColumns#DOC_ID}, and each root starts at the
* {@link Documents#DOC_ID_ROOT} document.
* <p>
* Documents can be either an openable file (with a specific MIME type), or a
* directory containing additional documents (with the
* {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
* capabilities, as described by {@link DocumentColumns#FLAGS}. The same
* {@link DocumentColumns#DOC_ID} can be included in multiple directories.
* <p>
* Document providers must be protected with the
* {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
* only be requested by the system. The system-provided UI then issues narrow
* Uri permission grants for individual documents when the user explicitly picks
* documents.
* To create a document provider, extend {@link DocumentsProvider}, which
* provides a foundational implementation of this contract.
*
* @see Intent#ACTION_OPEN_DOCUMENT
* @see Intent#ACTION_CREATE_DOCUMENT
* @see DocumentsProvider
*/
public final class DocumentsContract {
private static final String TAG = "Documents";
// content://com.example/roots/
// content://com.example/roots/sdcard/
// content://com.example/roots/sdcard/docs/0/
// content://com.example/roots/sdcard/docs/0/contents/
// content://com.example/roots/sdcard/docs/0/search/?query=pony
// content://com.example/docs/12/
// content://com.example/docs/12/children/
// content://com.example/docs/12/search/?query=pony
private DocumentsContract() {
}
/** {@hide} */
public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
/** {@hide} */
public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED";
public static final String ACTION_MANAGE_DOCUMENTS = "android.provider.action.MANAGE_DOCUMENTS";
/** {@hide} */
public static final String
ACTION_DOCUMENT_ROOT_CHANGED = "android.provider.action.DOCUMENT_ROOT_CHANGED";
/**
* Constants for individual documents.
*/
public static class Documents {
public final static class Documents {
private Documents() {
}
/**
* MIME type of a document which is a directory that may contain additional
* documents.
*
* @see #buildContentsUri(String, String, String)
*/
public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
/**
* {@link DocumentColumns#DOC_ID} value representing the root directory of a
* documents root.
*/
public static final String DOC_ID_ROOT = "0";
public static final String MIME_TYPE_DIR = "vnd.android.doc/dir";
/**
* Flag indicating that a document is a directory that supports creation of
* new files within it.
*
* @see DocumentColumns#FLAGS
* @see #createDocument(ContentResolver, Uri, String, String)
*/
public static final int FLAG_SUPPORTS_CREATE = 1;
@@ -126,7 +104,6 @@ public final class DocumentsContract {
* Flag indicating that a document is renamable.
*
* @see DocumentColumns#FLAGS
* @see #renameDocument(ContentResolver, Uri, String)
*/
public static final int FLAG_SUPPORTS_RENAME = 1 << 1;
@@ -141,7 +118,6 @@ public final class DocumentsContract {
* Flag indicating that a document can be represented as a thumbnail.
*
* @see DocumentColumns#FLAGS
* @see #getThumbnail(ContentResolver, Uri, Point)
*/
public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;
@@ -153,7 +129,7 @@ public final class DocumentsContract {
public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
/**
* Flag indicating that a document is writable.
* Flag indicating that a document supports writing.
*
* @see DocumentColumns#FLAGS
*/
@@ -169,128 +145,90 @@ public final class DocumentsContract {
public static final int FLAG_PREFERS_GRID = 1 << 6;
}
/**
* Optimal dimensions for a document thumbnail request, stored as a
* {@link Point} object. This is only a hint, and the returned thumbnail may
* have different dimensions.
*
* @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
*/
public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
/**
* Extra boolean flag included in a directory {@link Cursor#getExtras()}
* indicating that the document provider can provide additional data if
* requested, such as additional search results.
*/
public static final String EXTRA_HAS_MORE = "has_more";
/**
* Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
* directory to request that additional data should be fetched. When
* requested data is ready, the provider should send a change notification
* to cause a requery.
* indicating that a document provider is still loading data. For example, a
* provider has returned some results, but is still waiting on an
* outstanding network request.
*
* @see Cursor#respond(Bundle)
* @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
* boolean)
*/
public static final String EXTRA_REQUEST_MORE = "request_more";
public static final String EXTRA_LOADING = "loading";
/**
* 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_ROOTS = "roots";
private static final String PATH_DOCS = "docs";
private static final String PATH_CONTENTS = "contents";
private static final String PATH_CHILDREN = "children";
private static final String PATH_SEARCH = "search";
private static final String PARAM_QUERY = "query";
private static final String PARAM_LOCAL_ONLY = "localOnly";
/**
* Build Uri representing the roots offered by a document provider.
*/
public static Uri buildRootsUri(String authority) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).appendPath(PATH_ROOTS).build();
}
/**
* Build Uri representing a specific root offered by a document provider.
*/
public static Uri buildRootUri(String authority, String rootId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
}
/**
* Build Uri representing the given {@link DocumentColumns#DOC_ID} in a
* document provider.
*/
public static Uri buildDocumentUri(String authority, String rootId, String docId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
.appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
.build();
public static Uri buildDocumentUri(String authority, String docId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).appendPath(PATH_DOCS).appendPath(docId).build();
}
/**
* Build Uri representing the contents of the given directory in a document
* provider. The given document must be {@link Documents#MIME_TYPE_DIR}.
*
* @hide
*/
public static Uri buildContentsUri(String authority, String rootId, String docId) {
public static Uri buildChildrenUri(String authority, String docId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
.appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
.appendPath(PATH_CONTENTS).build();
.appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_CHILDREN).build();
}
/**
* Build Uri representing a search for matching documents under a specific
* directory in a document provider. The given document must have
* {@link Documents#FLAG_SUPPORTS_SEARCH}.
*
* @hide
*/
public static Uri buildSearchUri(String authority, String rootId, String docId, String query) {
public static Uri buildSearchUri(String authority, String docId, String query) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
.appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
.appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
}
/**
* Convenience method for {@link #buildDocumentUri(String, String, String)},
* extracting authority and root from the given Uri.
*/
public static Uri buildDocumentUri(Uri relatedUri, String docId) {
return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId);
}
/**
* Convenience method for {@link #buildContentsUri(String, String, String)},
* extracting authority and root from the given Uri.
*/
public static Uri buildContentsUri(Uri relatedUri) {
return buildContentsUri(
relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri));
}
/**
* Convenience method for
* {@link #buildSearchUri(String, String, String, String)}, extracting
* authority and root from the given Uri.
*/
public static Uri buildSearchUri(Uri relatedUri, String query) {
return buildSearchUri(
relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query);
}
/**
* Extract the {@link RootColumns#ROOT_ID} from the given Uri.
*/
public static String getRootId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
if (paths.size() < 2) {
throw new IllegalArgumentException("Not a root: " + documentUri);
}
if (!PATH_ROOTS.equals(paths.get(0))) {
throw new IllegalArgumentException("Not a root: " + documentUri);
}
return paths.get(1);
.appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_SEARCH)
.appendQueryParameter(PARAM_QUERY, query).build();
}
/**
@@ -298,68 +236,35 @@ public final class DocumentsContract {
*/
public static String getDocId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
if (paths.size() < 4) {
if (paths.size() < 2) {
throw new IllegalArgumentException("Not a document: " + documentUri);
}
if (!PATH_ROOTS.equals(paths.get(0))) {
if (!PATH_DOCS.equals(paths.get(0))) {
throw new IllegalArgumentException("Not a document: " + documentUri);
}
if (!PATH_DOCS.equals(paths.get(2))) {
throw new IllegalArgumentException("Not a document: " + documentUri);
}
return paths.get(3);
return paths.get(1);
}
/**
* Return requested search query from the given Uri, as constructed by
* {@link #buildSearchUri(String, String, String, String)}.
*/
/** {@hide} */
public static String getSearchQuery(Uri documentUri) {
return documentUri.getQueryParameter(PARAM_QUERY);
}
/**
* Mark the given Uri to indicate that only locally-available data should be
* returned. That is, no network connections should be initiated to provide
* the metadata or content.
*/
public static Uri setLocalOnly(Uri documentUri) {
return documentUri.buildUpon()
.appendQueryParameter(PARAM_LOCAL_ONLY, String.valueOf(true)).build();
}
/**
* Return if the given Uri is requesting that only locally-available data be
* returned. That is, no network connections should be initiated to provide
* the metadata or content.
*/
public static boolean isLocalOnly(Uri documentUri) {
return documentUri.getBooleanQueryParameter(PARAM_LOCAL_ONLY, false);
}
/**
* Standard columns for document queries. Document providers <em>must</em>
* support at least these columns when queried.
*
* @see DocumentsContract#buildDocumentUri(String, String, String)
* @see DocumentsContract#buildContentsUri(String, String, String)
* @see DocumentsContract#buildSearchUri(String, String, String, String)
*/
public interface DocumentColumns extends OpenableColumns {
/**
* The ID for a document under a storage backend root. Values
* <em>must</em> never change once returned. This field is read-only to
* document clients.
* Unique ID for a document. Values <em>must</em> never change once
* returned, since they may used for long-term Uri permission grants.
* <p>
* Type: STRING
*/
public static final String DOC_ID = "doc_id";
/**
* MIME type of a document, matching the value returned by
* {@link ContentResolver#getType(android.net.Uri)}. This field must be
* provided when a new document is created. This field is read-only to
* document clients.
* MIME type of a document.
* <p>
* Type: STRING
*
@@ -369,10 +274,10 @@ public final class DocumentsContract {
/**
* Timestamp when a document was last modified, in milliseconds since
* January 1, 1970 00:00:00.0 UTC. This field is read-only to document
* clients. Document providers can update this field using events from
* January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. Document
* providers can update this field using events from
* {@link OnCloseListener} or other reliable
* {@link ParcelFileDescriptor} transport.
* {@link ParcelFileDescriptor} transports.
* <p>
* Type: INTEGER (long)
*
@@ -381,37 +286,37 @@ public final class DocumentsContract {
public static final String LAST_MODIFIED = "last_modified";
/**
* Flags that apply to a specific document. This field is read-only to
* document clients.
* Specific icon resource for a document, or {@code null} to resolve
* default using {@link #MIME_TYPE}.
* <p>
* Type: INTEGER (int)
*/
public static final String FLAGS = "flags";
public static final String ICON = "icon";
/**
* Summary for this document, or {@code null} to omit. This field is
* read-only to document clients.
* Summary for a document, or {@code null} to omit.
* <p>
* Type: STRING
*/
public static final String SUMMARY = "summary";
/**
* Flags that apply to a specific document.
* <p>
* Type: INTEGER (int)
*/
public static final String FLAGS = "flags";
}
/**
* Constants for individual document roots.
* Metadata about a specific root of documents.
*/
public static class Roots {
private Roots() {
}
public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
public final static class DocumentRoot implements Parcelable {
/**
* Root that represents a storage service, such as a cloud-based
* service.
*
* @see RootColumns#ROOT_TYPE
* @see #rootType
*/
public static final int ROOT_TYPE_SERVICE = 1;
@@ -419,14 +324,14 @@ public final class DocumentsContract {
* Root that represents a shortcut to content that may be available
* elsewhere through another storage root.
*
* @see RootColumns#ROOT_TYPE
* @see #rootType
*/
public static final int ROOT_TYPE_SHORTCUT = 2;
/**
* Root that represents a physical storage device.
*
* @see RootColumns#ROOT_TYPE
* @see #rootType
*/
public static final int ROOT_TYPE_DEVICE = 3;
@@ -434,65 +339,154 @@ public final class DocumentsContract {
* Root that represents a physical storage device that should only be
* displayed to advanced users.
*
* @see RootColumns#ROOT_TYPE
* @see #rootType
*/
public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
}
/**
* Standard columns for document root queries.
*
* @see DocumentsContract#buildRootsUri(String)
* @see DocumentsContract#buildRootUri(String, String)
*/
public interface RootColumns {
public static final String ROOT_ID = "root_id";
/**
* Storage root type, use for clustering. This field is read-only to
* document clients.
* <p>
* Type: INTEGER (int)
* Flag indicating that at least one directory under this root supports
* creating content.
*
* @see Roots#ROOT_TYPE_SERVICE
* @see Roots#ROOT_TYPE_DEVICE
* @see #flags
*/
public static final String ROOT_TYPE = "root_type";
public static final int FLAG_SUPPORTS_CREATE = 1;
/**
* Icon resource ID for this storage root, or {@code null} to use the
* default {@link ProviderInfo#icon}. This field is read-only to
* document clients.
* <p>
* Type: INTEGER (int)
* Flag indicating that this root offers content that is strictly local
* on the device. That is, no network requests are made for the content.
*
* @see #flags
*/
public static final String ICON = "icon";
public static final int FLAG_LOCAL_ONLY = 1 << 1;
/** {@hide} */
public String authority;
/**
* Title for this storage root, or {@code null} to use the default
* {@link ProviderInfo#labelRes}. This field is read-only to document
* clients.
* <p>
* Type: STRING
* Root type, use for clustering.
*
* @see #ROOT_TYPE_SERVICE
* @see #ROOT_TYPE_DEVICE
*/
public static final String TITLE = "title";
public int rootType;
/**
* Summary for this storage root, or {@code null} to omit. This field is
* read-only to document clients.
* <p>
* Type: STRING
* Flags for this root.
*
* @see #FLAG_LOCAL_ONLY
*/
public static final String SUMMARY = "summary";
public int flags;
/**
* Number of free bytes of available in this storage root, or
* {@code null} if unknown or unbounded. This field is read-only to
* document clients.
* <p>
* Type: INTEGER (long)
* Icon resource ID for this root.
*/
public static final String AVAILABLE_BYTES = "available_bytes";
public int icon;
/**
* Title for this root.
*/
public String title;
/**
* Summary for this root. May be {@code null}.
*/
public String summary;
/**
* Document which is a directory that represents the top of this root.
* Must not be {@code null}.
*
* @see DocumentColumns#DOC_ID
*/
public String docId;
/**
* Document which is a directory representing recently modified
* documents under this root. This directory should return at most two
* dozen documents modified within the last 90 days. May be {@code null}
* if this root doesn't support recents.
*
* @see DocumentColumns#DOC_ID
*/
public String recentDocId;
/**
* Number of free bytes of available in this root, or -1 if unknown or
* unbounded.
*/
public long availableBytes;
/**
* Set of MIME type filters describing the content offered by this root,
* or {@code null} to indicate that all MIME types are supported. For
* example, a provider only supporting audio and video might set this to
* {@code ["audio/*", "video/*"]}.
*/
public String[] mimeTypes;
public DocumentRoot() {
}
/** {@hide} */
public DocumentRoot(Parcel in) {
rootType = in.readInt();
flags = in.readInt();
icon = in.readInt();
title = in.readString();
summary = in.readString();
docId = in.readString();
recentDocId = in.readString();
availableBytes = in.readLong();
mimeTypes = in.readStringArray();
}
/** {@hide} */
public Drawable loadIcon(Context context) {
if (icon != 0) {
if (authority != null) {
final PackageManager pm = context.getPackageManager();
final ProviderInfo info = pm.resolveContentProvider(authority, 0);
if (info != null) {
return pm.getDrawable(info.packageName, icon, info.applicationInfo);
}
} else {
return context.getResources().getDrawable(icon);
}
}
return null;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
Preconditions.checkNotNull(docId);
dest.writeInt(rootType);
dest.writeInt(flags);
dest.writeInt(icon);
dest.writeString(title);
dest.writeString(summary);
dest.writeString(docId);
dest.writeString(recentDocId);
dest.writeLong(availableBytes);
dest.writeStringArray(mimeTypes);
}
public static final Creator<DocumentRoot> CREATOR = new Creator<DocumentRoot>() {
@Override
public DocumentRoot createFromParcel(Parcel in) {
return new DocumentRoot(in);
}
@Override
public DocumentRoot[] newArray(int size) {
return new DocumentRoot[size];
}
};
}
/**
@@ -531,6 +525,7 @@ public final class DocumentsContract {
* {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set.
*
* @return decoded thumbnail, or {@code null} if problem was encountered.
* @hide
*/
public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
final Bundle openOpts = new Bundle();
@@ -588,44 +583,83 @@ public final class DocumentsContract {
}
}
/**
* Create a new document under a specific parent document with the given
* display name and MIME type.
*
* @param parentDocumentUri document with
* {@link Documents#FLAG_SUPPORTS_CREATE}
* @param displayName name for new document
* @param mimeType MIME type for new document, which cannot be changed
* @return newly created document Uri, or {@code null} if failed
*/
public static Uri createDocument(
ContentResolver resolver, Uri parentDocumentUri, String displayName, String mimeType) {
final ContentValues values = new ContentValues();
values.put(DocumentColumns.MIME_TYPE, mimeType);
values.put(DocumentColumns.DISPLAY_NAME, displayName);
return resolver.insert(parentDocumentUri, values);
/** {@hide} */
public static List<DocumentRoot> getDocumentRoots(ContentProviderClient client) {
try {
final Bundle out = client.call(METHOD_GET_ROOTS, null, null);
final List<DocumentRoot> roots = out.getParcelableArrayList(EXTRA_ROOTS);
return roots;
} catch (Exception e) {
Log.w(TAG, "Failed to get roots", e);
return null;
}
}
/**
* Rename the document at the given URI. Given document must have
* {@link Documents#FLAG_SUPPORTS_RENAME} set.
* Create a new document under the given parent document with MIME type and
* display name.
*
* @return if rename was successful.
* @param docId document with {@link Documents#FLAG_SUPPORTS_CREATE}
* @param mimeType MIME type of new document
* @param displayName name of new document
* @return newly created document, or {@code null} if failed
* @hide
*/
public static boolean renameDocument(
ContentResolver resolver, Uri documentUri, String displayName) {
final ContentValues values = new ContentValues();
values.put(DocumentColumns.DISPLAY_NAME, displayName);
return (resolver.update(documentUri, values, null, null) == 1);
public static String createDocument(
ContentProviderClient client, String docId, String mimeType, String displayName) {
final Bundle in = new Bundle();
in.putString(DocumentColumns.DOC_ID, docId);
in.putString(DocumentColumns.MIME_TYPE, mimeType);
in.putString(DocumentColumns.DISPLAY_NAME, displayName);
try {
final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
return out.getString(DocumentColumns.DOC_ID);
} catch (Exception e) {
Log.w(TAG, "Failed to create document", e);
return null;
}
}
/**
* Notify the system that roots have changed for the given storage provider.
* This signal is used to invalidate internal caches.
* Rename the given document.
*
* @param docId document with {@link Documents#FLAG_SUPPORTS_RENAME}
* @return document which may have changed due to rename, or {@code null} if
* rename failed.
* @hide
*/
public static void notifyRootsChanged(Context context, String authority) {
final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED);
intent.setData(buildRootsUri(authority));
context.sendBroadcast(intent);
public static String renameDocument(
ContentProviderClient client, String docId, String displayName) {
final Bundle in = new Bundle();
in.putString(DocumentColumns.DOC_ID, docId);
in.putString(DocumentColumns.DISPLAY_NAME, displayName);
try {
final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in);
return out.getString(DocumentColumns.DOC_ID);
} catch (Exception e) {
Log.w(TAG, "Failed to rename document", e);
return null;
}
}
/**
* Delete the given document.
*
* @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE}
* @hide
*/
public static boolean deleteDocument(ContentProviderClient client, String docId) {
final Bundle in = new Bundle();
in.putString(DocumentColumns.DOC_ID, docId);
try {
client.call(METHOD_DELETE_DOCUMENT, null, in);
return true;
} catch (Exception e) {
Log.w(TAG, "Failed to delete document", e);
return false;
}
}
}

View File

@@ -0,0 +1,384 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.provider;
import static android.provider.DocumentsContract.ACTION_DOCUMENT_ROOT_CHANGED;
import static android.provider.DocumentsContract.EXTRA_AUTHORITY;
import static android.provider.DocumentsContract.EXTRA_ROOTS;
import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE;
import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_GET_ROOTS;
import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT;
import static android.provider.DocumentsContract.getDocId;
import static android.provider.DocumentsContract.getSearchQuery;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract.DocumentRoot;
import android.provider.DocumentsContract.Documents;
import android.util.Log;
import libcore.io.IoUtils;
import java.io.FileNotFoundException;
import java.util.List;
/**
* Base class for a document provider. A document provider should extend this
* class and implement the abstract methods.
* <p>
* Each document provider expresses one or more "roots" which each serve as the
* top-level of a tree. For example, a root could represent an account, or a
* physical storage device. Under each root, documents are referenced by
* {@link DocumentColumns#DOC_ID}, which must not change once returned.
* <p>
* Documents can be either an openable file (with a specific MIME type), or a
* directory containing additional documents (with the
* {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
* capabilities, as described by {@link DocumentColumns#FLAGS}. The same
* {@link DocumentColumns#DOC_ID} can be included in multiple directories.
* <p>
* Document providers must be protected with the
* {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
* only be requested by the system. The system-provided UI then issues narrow
* Uri permission grants for individual documents when the user explicitly picks
* documents.
*
* @see Intent#ACTION_OPEN_DOCUMENT
* @see Intent#ACTION_CREATE_DOCUMENT
*/
public abstract class DocumentsProvider extends ContentProvider {
private static final String TAG = "DocumentsProvider";
private static final int MATCH_DOCUMENT = 1;
private static final int MATCH_CHILDREN = 2;
private static final int MATCH_SEARCH = 3;
private String mAuthority;
private UriMatcher mMatcher;
@Override
public void attachInfo(Context context, ProviderInfo info) {
mAuthority = info.authority;
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(mAuthority, "docs/*", MATCH_DOCUMENT);
mMatcher.addURI(mAuthority, "docs/*/children", MATCH_CHILDREN);
mMatcher.addURI(mAuthority, "docs/*/search", MATCH_SEARCH);
// Sanity check our setup
if (!info.exported) {
throw new SecurityException("Provider must be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grantUriPermissions");
}
if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
|| !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
}
super.attachInfo(context, info);
}
/**
* Return list of all document roots provided by this document provider.
* When this list changes, a provider must call
* {@link #notifyDocumentRootsChanged()}.
*/
public abstract List<DocumentRoot> getDocumentRoots();
/**
* Create and return a new document. A provider must allocate a new
* {@link DocumentColumns#DOC_ID} to represent the document, which must not
* change once returned.
*
* @param docId the parent directory to create the new document under.
* @param mimeType the MIME type associated with the new document.
* @param displayName the display name of the new document.
*/
@SuppressWarnings("unused")
public String createDocument(String docId, String mimeType, String displayName)
throws FileNotFoundException {
throw new UnsupportedOperationException("Create not supported");
}
/**
* Rename the given document.
*
* @param docId the document to rename.
* @param displayName the new display name.
*/
@SuppressWarnings("unused")
public void renameDocument(String docId, String displayName) throws FileNotFoundException {
throw new UnsupportedOperationException("Rename not supported");
}
/**
* Delete the given document.
*
* @param docId the document to delete.
*/
@SuppressWarnings("unused")
public void deleteDocument(String docId) throws FileNotFoundException {
throw new UnsupportedOperationException("Delete not supported");
}
/**
* Return metadata for the given document. A provider should avoid making
* network requests to keep this request fast.
*
* @param docId the document to return.
*/
public abstract Cursor queryDocument(String docId) throws FileNotFoundException;
/**
* Return the children of the given document which is a directory.
*
* @param docId the directory to return children for.
*/
public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException;
/**
* Return documents that that match the given query, starting the search at
* the given directory.
*
* @param docId the directory to start search at.
*/
@SuppressWarnings("unused")
public Cursor querySearch(String docId, String query) throws FileNotFoundException {
throw new UnsupportedOperationException("Search not supported");
}
/**
* Return MIME type for the given document. Must match the value of
* {@link DocumentColumns#MIME_TYPE} for this document.
*/
public String getType(String docId) throws FileNotFoundException {
final Cursor cursor = queryDocument(docId);
try {
if (cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE));
} else {
return null;
}
} finally {
IoUtils.closeQuietly(cursor);
}
}
/**
* Open and return the requested document. A provider should return a
* reliable {@link ParcelFileDescriptor} to detect when the remote caller
* has finished reading or writing the document. A provider may return a
* pipe or socket pair if the mode is exclusively
* {@link ParcelFileDescriptor#MODE_READ_ONLY} or
* {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
* {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
* disk. If a provider blocks while downloading content, it should
* periodically check {@link CancellationSignal#isCanceled()} to abort
* abandoned open requests.
*
* @param docId the document to return.
* @param mode the mode to open with, such as 'r', 'w', or 'rw'.
* @param signal used by the caller to signal if the request should be
* cancelled.
* @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
* OnCloseListener)
* @see ParcelFileDescriptor#createReliablePipe()
* @see ParcelFileDescriptor#createReliableSocketPair()
*/
public abstract ParcelFileDescriptor openDocument(
String docId, String mode, CancellationSignal signal) throws FileNotFoundException;
/**
* Open and return a thumbnail of the requested document. A provider should
* return a thumbnail closely matching the hinted size, attempting to serve
* from a local cache if possible. A provider should never return images
* more than double the hinted size. If a provider performs expensive
* operations to download or generate a thumbnail, it should periodically
* check {@link CancellationSignal#isCanceled()} to abort abandoned
* thumbnail requests.
*
* @param docId the document to return.
* @param sizeHint hint of the optimal thumbnail dimensions.
* @param signal used by the caller to signal if the request should be
* cancelled.
* @see Documents#FLAG_SUPPORTS_THUMBNAIL
*/
@SuppressWarnings("unused")
public AssetFileDescriptor openDocumentThumbnail(
String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
throw new UnsupportedOperationException("Thumbnails not supported");
}
@Override
public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
try {
switch (mMatcher.match(uri)) {
case MATCH_DOCUMENT:
return queryDocument(getDocId(uri));
case MATCH_CHILDREN:
return queryDocumentChildren(getDocId(uri));
case MATCH_SEARCH:
return querySearch(getDocId(uri), getSearchQuery(uri));
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
} catch (FileNotFoundException e) {
Log.w(TAG, "Failed during query", e);
return null;
}
}
@Override
public final String getType(Uri uri) {
try {
switch (mMatcher.match(uri)) {
case MATCH_DOCUMENT:
return getType(getDocId(uri));
default:
return null;
}
} catch (FileNotFoundException e) {
Log.w(TAG, "Failed during getType", e);
return null;
}
}
@Override
public final Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("Insert not supported");
}
@Override
public final int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Delete not supported");
}
@Override
public final int update(
Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Update not supported");
}
@Override
public final Bundle callFromPackage(
String callingPackage, String method, String arg, Bundle extras) {
if (!method.startsWith("android:")) {
// Let non-platform methods pass through
return super.callFromPackage(callingPackage, method, arg, extras);
}
// Platform operations require the caller explicitly hold manage
// permission; Uri permissions don't extend management operations.
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.MANAGE_DOCUMENTS, "Document management");
final Bundle out = new Bundle();
try {
if (METHOD_GET_ROOTS.equals(method)) {
final List<DocumentRoot> roots = getDocumentRoots();
out.putParcelableList(EXTRA_ROOTS, roots);
} else if (METHOD_CREATE_DOCUMENT.equals(method)) {
final String docId = extras.getString(DocumentColumns.DOC_ID);
final String mimeType = extras.getString(DocumentColumns.MIME_TYPE);
final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
// TODO: issue Uri grant towards caller
final String newDocId = createDocument(docId, mimeType, displayName);
out.putString(DocumentColumns.DOC_ID, newDocId);
} else if (METHOD_RENAME_DOCUMENT.equals(method)) {
final String docId = extras.getString(DocumentColumns.DOC_ID);
final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
renameDocument(docId, displayName);
} else if (METHOD_DELETE_DOCUMENT.equals(method)) {
final String docId = extras.getString(DocumentColumns.DOC_ID);
deleteDocument(docId);
} else {
throw new UnsupportedOperationException("Method not supported " + method);
}
} catch (FileNotFoundException e) {
throw new IllegalStateException("Failed call " + method, e);
}
return out;
}
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return openDocument(getDocId(uri), mode, null);
}
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
throws FileNotFoundException {
return openDocument(getDocId(uri), mode, signal);
}
@Override
public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocId(uri), sizeHint, null);
} else {
return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
}
}
@Override
public final AssetFileDescriptor openTypedAssetFile(
Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
throws FileNotFoundException {
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocId(uri), sizeHint, signal);
} else {
return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
}
}
/**
* Notify system that {@link #getDocumentRoots()} has changed, usually due to an
* account or device change.
*/
public void notifyDocumentRootsChanged() {
final Intent intent = new Intent(ACTION_DOCUMENT_ROOT_CHANGED);
intent.putExtra(EXTRA_AUTHORITY, mAuthority);
getContext().sendBroadcast(intent);
}
}

View File

@@ -35,9 +35,9 @@
</intent-filter>
<!-- data expected to point at existing root to manage -->
<intent-filter>
<action android:name="android.intent.action.MANAGE_DOCUMENT" />
<action android:name="android.provider.action.MANAGE_DOCUMENTS" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/root" />
<data android:mimeType="vnd.android.doc/dir" />
</intent-filter>
</activity>

View File

@@ -20,14 +20,14 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Documents;
import android.view.LayoutInflater;
import android.view.View;
@@ -36,8 +36,6 @@ import android.widget.Toast;
import com.android.documentsui.model.Document;
import java.io.FileNotFoundException;
/**
* Dialog to create a new directory.
*/
@@ -58,7 +56,7 @@ public class CreateDirectoryFragment extends DialogFragment {
final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
final EditText text1 = (EditText) view.findViewById(android.R.id.text1);
builder.setTitle(R.string.menu_create_dir);
builder.setView(view);
@@ -68,24 +66,25 @@ public class CreateDirectoryFragment extends DialogFragment {
public void onClick(DialogInterface dialog, int which) {
final String displayName = text1.getText().toString();
final ContentValues values = new ContentValues();
values.put(DocumentColumns.MIME_TYPE, Documents.MIME_TYPE_DIR);
values.put(DocumentColumns.DISPLAY_NAME, displayName);
final DocumentsActivity activity = (DocumentsActivity) getActivity();
final Document cwd = activity.getCurrentDirectory();
Uri childUri = resolver.insert(cwd.uri, values);
final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
cwd.uri.getAuthority());
try {
final String docId = DocumentsContract.createDocument(client,
DocumentsContract.getDocId(cwd.uri), Documents.MIME_TYPE_DIR,
displayName);
// Navigate into newly created child
final Uri childUri = DocumentsContract.buildDocumentUri(
cwd.uri.getAuthority(), docId);
final Document childDoc = Document.fromUri(resolver, childUri);
activity.onDocumentPicked(childDoc);
} catch (FileNotFoundException e) {
childUri = null;
}
if (childUri == null) {
} catch (Exception e) {
Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
} finally {
ContentProviderClient.closeQuietly(client);
}
}
});

View File

@@ -20,8 +20,8 @@ import static com.android.documentsui.DocumentsActivity.TAG;
import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DATE;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_NAME;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DISPLAY_NAME;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_SIZE;
import android.app.Fragment;
@@ -32,7 +32,6 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
@@ -55,7 +54,6 @@ import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListView;
@@ -64,7 +62,6 @@ import android.widget.Toast;
import com.android.documentsui.DocumentsActivity.DisplayState;
import com.android.documentsui.model.Document;
import com.android.documentsui.model.Root;
import com.android.internal.util.Predicate;
import com.google.android.collect.Lists;
@@ -81,7 +78,6 @@ public class DirectoryFragment extends Fragment {
private View mEmptyView;
private ListView mListView;
private GridView mGridView;
private Button mMoreView;
private AbsListView mCurrentView;
@@ -110,7 +106,8 @@ public class DirectoryFragment extends Fragment {
}
public static void showSearch(FragmentManager fm, Uri uri, String query) {
final Uri searchUri = DocumentsContract.buildSearchUri(uri, query);
final Uri searchUri = DocumentsContract.buildSearchUri(
uri.getAuthority(), DocumentsContract.getDocId(uri), query);
show(fm, TYPE_SEARCH, searchUri);
}
@@ -153,8 +150,6 @@ public class DirectoryFragment extends Fragment {
mGridView.setOnItemClickListener(mItemListener);
mGridView.setMultiChoiceModeListener(mMultiListener);
mMoreView = (Button) view.findViewById(R.id.more);
mAdapter = new DocumentsAdapter();
final Uri uri = getArguments().getParcelable(EXTRA_URI);
@@ -168,22 +163,19 @@ public class DirectoryFragment extends Fragment {
Uri contentsUri;
if (mType == TYPE_NORMAL) {
contentsUri = DocumentsContract.buildContentsUri(uri);
contentsUri = DocumentsContract.buildChildrenUri(
uri.getAuthority(), DocumentsContract.getDocId(uri));
} else if (mType == TYPE_RECENT_OPEN) {
contentsUri = RecentsProvider.buildRecentOpen();
} else {
contentsUri = uri;
}
if (state.localOnly) {
contentsUri = DocumentsContract.setLocalOnly(contentsUri);
}
final Comparator<Document> sortOrder;
if (state.sortOrder == SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
sortOrder = new Document.DateComparator();
} else if (state.sortOrder == SORT_ORDER_NAME) {
sortOrder = new Document.NameComparator();
if (state.sortOrder == SORT_ORDER_LAST_MODIFIED || mType == TYPE_RECENT_OPEN) {
sortOrder = new Document.LastModifiedComparator();
} else if (state.sortOrder == SORT_ORDER_DISPLAY_NAME) {
sortOrder = new Document.DisplayNameComparator();
} else if (state.sortOrder == SORT_ORDER_SIZE) {
sortOrder = new Document.SizeComparator();
} else {
@@ -196,28 +188,6 @@ public class DirectoryFragment extends Fragment {
@Override
public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
mAdapter.swapDocuments(result.contents);
final Cursor cursor = result.cursor;
if (cursor != null && cursor.getExtras()
.getBoolean(DocumentsContract.EXTRA_HAS_MORE, false)) {
mMoreView.setText(R.string.more);
mMoreView.setVisibility(View.VISIBLE);
mMoreView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMoreView.setText(R.string.loading);
final Bundle bundle = new Bundle();
bundle.putBoolean(DocumentsContract.EXTRA_REQUEST_MORE, true);
try {
cursor.respond(bundle);
} catch (Exception e) {
Log.w(TAG, "Failed to respond: " + e);
}
}
});
} else {
mMoreView.setVisibility(View.GONE);
}
}
@Override
@@ -489,8 +459,7 @@ public class DirectoryFragment extends Fragment {
task.execute(doc.uri);
}
} else {
icon.setImageDrawable(roots.resolveDocumentIcon(
context, doc.uri.getAuthority(), doc.mimeType));
icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, doc.mimeType));
}
title.setText(doc.displayName);
@@ -504,11 +473,7 @@ public class DirectoryFragment extends Fragment {
summary.setVisibility(View.INVISIBLE);
}
} else if (mType == TYPE_RECENT_OPEN) {
final Root root = roots.findRoot(doc);
icon1.setVisibility(View.VISIBLE);
icon1.setImageDrawable(root.icon);
summary.setText(root.getDirectoryString());
summary.setVisibility(View.VISIBLE);
// TODO: resolve storage root
}
if (summaryGrid != null) {

View File

@@ -26,7 +26,6 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.CancellationSignal;
import android.provider.DocumentsContract.DocumentColumns;
import android.util.Log;
import com.android.documentsui.model.Document;
@@ -77,9 +76,10 @@ public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> {
}
private void loadInBackgroundInternal(
DirectoryResult result, Uri uri, CancellationSignal signal) {
DirectoryResult result, Uri uri, CancellationSignal signal) throws RuntimeException {
// TODO: switch to using unstable CPC
final ContentResolver resolver = getContext().getContentResolver();
final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal);
final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
result.cursor = cursor;
result.cursor.registerContentObserver(mObserver);
@@ -110,16 +110,4 @@ public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> {
Collections.sort(result.contents, mSortOrder);
}
}
private String getQuerySortOrder() {
if (mSortOrder instanceof Document.DateComparator) {
return DocumentColumns.LAST_MODIFIED + " DESC";
} else if (mSortOrder instanceof Document.NameComparator) {
return DocumentColumns.DISPLAY_NAME + " ASC";
} else if (mSortOrder instanceof Document.SizeComparator) {
return DocumentColumns.SIZE + " DESC";
} else {
return null;
}
}
}

View File

@@ -21,12 +21,11 @@ import static com.android.documentsui.DocumentsActivity.TAG;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.provider.DocumentsContract.DocumentRoot;
import android.util.Log;
import com.android.documentsui.model.Root;
/**
* Handles {@link Root} changes which invalidate cached data.
* Handles {@link DocumentRoot} changes which invalidate cached data.
*/
public class DocumentChangedReceiver extends BroadcastReceiver {
@Override

View File

@@ -22,7 +22,7 @@ import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANA
import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_OPEN;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DATE;
import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
import android.app.ActionBar;
import android.app.ActionBar.OnNavigationListener;
@@ -32,6 +32,7 @@ import android.app.FragmentManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
@@ -41,7 +42,7 @@ import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract.DocumentRoot;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
@@ -61,7 +62,6 @@ import android.widget.Toast;
import com.android.documentsui.model.Document;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.Root;
import java.io.FileNotFoundException;
import java.util.Arrays;
@@ -101,7 +101,7 @@ public class DocumentsActivity extends Activity {
mAction = ACTION_CREATE;
} else if (Intent.ACTION_GET_CONTENT.equals(action)) {
mAction = ACTION_GET_CONTENT;
} else if (Intent.ACTION_MANAGE_DOCUMENT.equals(action)) {
} else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) {
mAction = ACTION_MANAGE;
}
@@ -143,7 +143,7 @@ public class DocumentsActivity extends Activity {
}
if (mAction == ACTION_MANAGE) {
mDisplayState.sortOrder = SORT_ORDER_DATE;
mDisplayState.sortOrder = SORT_ORDER_LAST_MODIFIED;
}
mRootsContainer = findViewById(R.id.container_roots);
@@ -160,10 +160,7 @@ public class DocumentsActivity extends Activity {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
final Uri rootUri = intent.getData();
final String authority = rootUri.getAuthority();
final String rootId = DocumentsContract.getRootId(rootUri);
final Root root = mRoots.findRoot(authority, rootId);
final DocumentRoot root = mRoots.findRoot(rootUri);
if (root != null) {
onRootPicked(root, true);
} else {
@@ -255,10 +252,10 @@ public class DocumentsActivity extends Activity {
mDrawerToggle.setDrawerIndicatorEnabled(true);
} else {
final Root root = getCurrentRoot();
actionBar.setIcon(root != null ? root.icon : null);
final DocumentRoot root = getCurrentRoot();
actionBar.setIcon(root != null ? root.loadIcon(this) : null);
if (root.isRecents) {
if (mRoots.isRecentsRoot(root)) {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setTitle(root.title);
} else {
@@ -441,9 +438,8 @@ public class DocumentsActivity extends Activity {
final TextView title = (TextView) convertView.findViewById(android.R.id.title);
final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
final Document cwd = getCurrentDirectory();
if (cwd != null) {
title.setText(cwd.displayName);
if (mStack.size() > 0) {
title.setText(mStack.getTitle(mRoots));
} else {
// No directory means recents
title.setText(R.string.root_recent);
@@ -477,10 +473,9 @@ public class DocumentsActivity extends Activity {
}
};
public Root getCurrentRoot() {
final Document cwd = getCurrentDirectory();
if (cwd != null) {
return mRoots.findRoot(cwd);
public DocumentRoot getCurrentRoot() {
if (mStack.size() > 0) {
return mStack.getRoot(mRoots);
} else {
return mRoots.getRecentsRoot();
}
@@ -538,13 +533,14 @@ public class DocumentsActivity extends Activity {
onCurrentDirectoryChanged();
}
public void onRootPicked(Root root, boolean closeDrawer) {
public void onRootPicked(DocumentRoot root, boolean closeDrawer) {
// Clear entire backstack and start in new root
mStack.clear();
if (!root.isRecents) {
if (!mRoots.isRecentsRoot(root)) {
try {
onDocumentPicked(Document.fromRoot(getContentResolver(), root));
final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.docId);
onDocumentPicked(Document.fromUri(getContentResolver(), uri));
} catch (FileNotFoundException e) {
}
} else {
@@ -611,16 +607,21 @@ public class DocumentsActivity extends Activity {
}
public void onSaveRequested(String mimeType, String displayName) {
final ContentValues values = new ContentValues();
values.put(DocumentColumns.MIME_TYPE, mimeType);
values.put(DocumentColumns.DISPLAY_NAME, displayName);
final Document cwd = getCurrentDirectory();
final Uri childUri = getContentResolver().insert(cwd.uri, values);
if (childUri != null) {
final String authority = cwd.uri.getAuthority();
final ContentProviderClient client = getContentResolver()
.acquireUnstableContentProviderClient(authority);
try {
final String docId = DocumentsContract.createDocument(client,
DocumentsContract.getDocId(cwd.uri), mimeType, displayName);
final Uri childUri = DocumentsContract.buildDocumentUri(authority, docId);
onFinished(childUri);
} else {
} catch (Exception e) {
Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
} finally {
ContentProviderClient.closeQuietly(client);
}
}
@@ -680,7 +681,7 @@ public class DocumentsActivity extends Activity {
public int action;
public int mode = MODE_LIST;
public String[] acceptMimes;
public int sortOrder = SORT_ORDER_NAME;
public int sortOrder = SORT_ORDER_DISPLAY_NAME;
public boolean allowMultiple = false;
public boolean showSize = false;
public boolean localOnly = false;
@@ -693,8 +694,8 @@ public class DocumentsActivity extends Activity {
public static final int MODE_LIST = 0;
public static final int MODE_GRID = 1;
public static final int SORT_ORDER_NAME = 0;
public static final int SORT_ORDER_DATE = 1;
public static final int SORT_ORDER_DISPLAY_NAME = 0;
public static final int SORT_ORDER_LAST_MODIFIED = 1;
public static final int SORT_ORDER_SIZE = 2;
}

View File

@@ -29,6 +29,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.provider.DocumentsContract.DocumentRoot;
import android.text.TextUtils.TruncateAt;
import android.util.Log;
import android.view.LayoutInflater;
@@ -42,7 +43,6 @@ import android.widget.ListView;
import android.widget.TextView;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.Root;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
@@ -181,8 +181,8 @@ public class RecentsCreateFragment extends Fragment {
final View summaryList = convertView.findViewById(R.id.summary_list);
final DocumentStack stack = getItem(position);
final Root root = roots.findRoot(stack.peek());
icon.setImageDrawable(root != null ? root.icon : null);
final DocumentRoot root = stack.getRoot(roots);
icon.setImageDrawable(root.loadIcon(context));
final StringBuilder builder = new StringBuilder();
for (int i = stack.size() - 1; i >= 0; i--) {

View File

@@ -18,30 +18,24 @@ package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentRoot;
import android.provider.DocumentsContract.Documents;
import android.util.Log;
import android.util.Pair;
import com.android.documentsui.model.Document;
import com.android.documentsui.model.DocumentsProviderInfo;
import com.android.documentsui.model.DocumentsProviderInfo.Icon;
import com.android.documentsui.model.Root;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Objects;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
/**
@@ -54,14 +48,9 @@ public class RootsCache {
private final Context mContext;
/** Map from authority to cached info */
private HashMap<String, DocumentsProviderInfo> mProviders = Maps.newHashMap();
/** Map from (authority+rootId) to cached info */
private HashMap<Pair<String, String>, Root> mRoots = Maps.newHashMap();
public List<DocumentRoot> mRoots = Lists.newArrayList();
public ArrayList<Root> mRootsList = Lists.newArrayList();
private Root mRecentsRoot;
private DocumentRoot mRecentsRoot;
public RootsCache(Context context) {
mContext = context;
@@ -73,95 +62,78 @@ public class RootsCache {
*/
@GuardedBy("ActivityThread")
public void update() {
mProviders.clear();
mRoots.clear();
mRootsList.clear();
{
// Create special root for recents
final Root root = Root.buildRecents(mContext);
mRootsList.add(root);
final DocumentRoot root = new DocumentRoot();
root.rootType = DocumentRoot.ROOT_TYPE_SHORTCUT;
root.docId = null;
root.icon = R.drawable.ic_dir;
root.title = mContext.getString(R.string.root_recent);
root.summary = null;
root.availableBytes = -1;
mRoots.add(root);
mRecentsRoot = root;
}
// Query for other storage backends
final ContentResolver resolver = mContext.getContentResolver();
final PackageManager pm = mContext.getPackageManager();
final List<ProviderInfo> providers = pm.queryContentProviders(
null, -1, PackageManager.GET_META_DATA);
for (ProviderInfo providerInfo : providers) {
if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
for (ProviderInfo info : providers) {
if (info.metaData != null && info.metaData.containsKey(
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
mContext, providerInfo);
if (info == null) {
Log.w(TAG, "Missing info for " + providerInfo);
continue;
}
mProviders.put(info.providerInfo.authority, info);
// TODO: remove deprecated customRoots flag
// TODO: populate roots on background thread, and cache results
final ContentProviderClient client = resolver
.acquireUnstableContentProviderClient(info.authority);
try {
// TODO: remove deprecated customRoots flag
// TODO: populate roots on background thread, and cache results
final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
final Cursor cursor = mContext.getContentResolver()
.query(uri, null, null, null, null);
try {
while (cursor.moveToNext()) {
final Root root = Root.fromCursor(mContext, info, cursor);
mRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
mRootsList.add(root);
}
} finally {
cursor.close();
final List<DocumentRoot> roots = DocumentsContract.getDocumentRoots(client);
for (DocumentRoot root : roots) {
root.authority = info.authority;
}
mRoots.addAll(roots);
} catch (Exception e) {
Log.w(TAG, "Failed to load some roots from " + info.providerInfo.authority
+ ": " + e);
Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e);
} finally {
ContentProviderClient.closeQuietly(client);
}
}
}
}
@GuardedBy("ActivityThread")
public DocumentsProviderInfo findProvider(String authority) {
return mProviders.get(authority);
public DocumentRoot findRoot(Uri uri) {
final String authority = uri.getAuthority();
final String docId = DocumentsContract.getDocId(uri);
for (DocumentRoot root : mRoots) {
if (Objects.equal(root.authority, authority) && Objects.equal(root.docId, docId)) {
return root;
}
}
return null;
}
@GuardedBy("ActivityThread")
public Root findRoot(String authority, String rootId) {
return mRoots.get(Pair.create(authority, rootId));
}
@GuardedBy("ActivityThread")
public Root findRoot(Document doc) {
final String authority = doc.uri.getAuthority();
final String rootId = DocumentsContract.getRootId(doc.uri);
return findRoot(authority, rootId);
}
@GuardedBy("ActivityThread")
public Root getRecentsRoot() {
public DocumentRoot getRecentsRoot() {
return mRecentsRoot;
}
@GuardedBy("ActivityThread")
public Collection<Root> getRoots() {
return mRootsList;
public boolean isRecentsRoot(DocumentRoot root) {
return mRecentsRoot == root;
}
@GuardedBy("ActivityThread")
public Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
// Custom icons take precedence
final DocumentsProviderInfo info = mProviders.get(authority);
if (info != null) {
for (Icon icon : info.customIcons) {
if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
return icon.icon;
}
}
}
public List<DocumentRoot> getRoots() {
return mRoots;
}
@GuardedBy("ActivityThread")
public static Drawable resolveDocumentIcon(Context context, String mimeType) {
if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
return context.getResources().getDrawable(R.drawable.ic_dir);
} else {

View File

@@ -26,7 +26,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.provider.DocumentsContract.Roots;
import android.provider.DocumentsContract.DocumentRoot;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
@@ -40,10 +40,9 @@ import android.widget.ListView;
import android.widget.TextView;
import com.android.documentsui.SectionedListAdapter.SectionAdapter;
import com.android.documentsui.model.Root;
import com.android.documentsui.model.Root.RootComparator;
import com.android.documentsui.model.Document;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
/**
@@ -102,8 +101,8 @@ public class RootsFragment extends Fragment {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
final Object item = mAdapter.getItem(position);
if (item instanceof Root) {
activity.onRootPicked((Root) item, true);
if (item instanceof DocumentRoot) {
activity.onRootPicked((DocumentRoot) item, true);
} else if (item instanceof ResolveInfo) {
activity.onAppPicked((ResolveInfo) item);
} else {
@@ -112,7 +111,7 @@ public class RootsFragment extends Fragment {
}
};
private static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
private static class RootsAdapter extends ArrayAdapter<DocumentRoot> implements SectionAdapter {
private int mHeaderId;
public RootsAdapter(Context context, int headerId) {
@@ -132,14 +131,14 @@ public class RootsFragment extends Fragment {
final TextView title = (TextView) convertView.findViewById(android.R.id.title);
final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
final Root root = getItem(position);
icon.setImageDrawable(root.icon);
final DocumentRoot root = getItem(position);
icon.setImageDrawable(root.loadIcon(context));
title.setText(root.title);
// Device summary is always available space
final String summaryText;
if ((root.rootType == Roots.ROOT_TYPE_DEVICE
|| root.rootType == Roots.ROOT_TYPE_DEVICE_ADVANCED)
if ((root.rootType == DocumentRoot.ROOT_TYPE_DEVICE
|| root.rootType == DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED)
&& root.availableBytes >= 0) {
summaryText = context.getString(R.string.root_available_bytes,
Formatter.formatFileSize(context, root.availableBytes));
@@ -216,27 +215,27 @@ public class RootsFragment extends Fragment {
private final RootsAdapter mDevicesAdvanced;
private final AppsAdapter mApps;
public SectionedRootsAdapter(Context context, Collection<Root> roots, Intent includeApps) {
public SectionedRootsAdapter(Context context, List<DocumentRoot> roots, Intent includeApps) {
mServices = new RootsAdapter(context, R.string.root_type_service);
mShortcuts = new RootsAdapter(context, R.string.root_type_shortcut);
mDevices = new RootsAdapter(context, R.string.root_type_device);
mDevicesAdvanced = new RootsAdapter(context, R.string.root_type_device);
mApps = new AppsAdapter(context);
for (Root root : roots) {
for (DocumentRoot root : roots) {
Log.d(TAG, "Found rootType=" + root.rootType);
switch (root.rootType) {
case Roots.ROOT_TYPE_SERVICE:
case DocumentRoot.ROOT_TYPE_SERVICE:
mServices.add(root);
break;
case Roots.ROOT_TYPE_SHORTCUT:
case DocumentRoot.ROOT_TYPE_SHORTCUT:
mShortcuts.add(root);
break;
case Roots.ROOT_TYPE_DEVICE:
case DocumentRoot.ROOT_TYPE_DEVICE:
mDevices.add(root);
mDevicesAdvanced.add(root);
break;
case Roots.ROOT_TYPE_DEVICE_ADVANCED:
case DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED:
mDevicesAdvanced.add(root);
break;
}
@@ -281,4 +280,16 @@ public class RootsFragment extends Fragment {
}
}
}
public static class RootComparator implements Comparator<DocumentRoot> {
@Override
public int compare(DocumentRoot lhs, DocumentRoot rhs) {
final int score = Document.compareToIgnoreCaseNullable(lhs.title, rhs.title);
if (score != 0) {
return score;
} else {
return Document.compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
}
}
}
}

View File

@@ -73,8 +73,8 @@ public class SaveFragment extends Fragment {
final View view = inflater.inflate(R.layout.fragment_save, container, false);
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
icon.setImageDrawable(roots.resolveDocumentIcon(
context, null, getArguments().getString(EXTRA_MIME_TYPE)));
icon.setImageDrawable(
RootsCache.resolveDocumentIcon(context, getArguments().getString(EXTRA_MIME_TYPE)));
mDisplayName = (EditText) view.findViewById(android.R.id.title);
mDisplayName.addTextChangedListener(mDisplayNameWatcher);

View File

@@ -53,17 +53,11 @@ public class Document {
this.size = size;
}
public static Document fromRoot(ContentResolver resolver, Root root)
throws FileNotFoundException {
return fromUri(resolver, root.uri);
}
public static Document fromDirectoryCursor(Uri parent, Cursor cursor) {
final String authority = parent.getAuthority();
final String rootId = DocumentsContract.getRootId(parent);
final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
final Uri uri = DocumentsContract.buildDocumentUri(authority, rootId, docId);
final Uri uri = DocumentsContract.buildDocumentUri(authority, docId);
final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
@@ -74,6 +68,7 @@ public class Document {
return new Document(uri, mimeType, displayName, lastModified, flags, summary, size);
}
@Deprecated
public static Document fromRecentOpenCursor(ContentResolver resolver, Cursor recentCursor)
throws FileNotFoundException {
final Uri uri = Uri.parse(getCursorString(recentCursor, RecentsProvider.COL_URI));
@@ -176,7 +171,7 @@ public class Document {
return (index != -1) ? cursor.getInt(index) : 0;
}
public static class NameComparator implements Comparator<Document> {
public static class DisplayNameComparator implements Comparator<Document> {
@Override
public int compare(Document lhs, Document rhs) {
final boolean leftDir = lhs.isDirectory();
@@ -185,12 +180,12 @@ public class Document {
if (leftDir != rightDir) {
return leftDir ? -1 : 1;
} else {
return Root.compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
return compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
}
}
}
public static class DateComparator implements Comparator<Document> {
public static class LastModifiedComparator implements Comparator<Document> {
@Override
public int compare(Document lhs, Document rhs) {
return Long.compare(rhs.lastModified, lhs.lastModified);
@@ -213,4 +208,10 @@ public class Document {
fnfe.initCause(t);
throw fnfe;
}
public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
if (lhs == null) return -1;
if (rhs == null) return 1;
return lhs.compareToIgnoreCase(rhs);
}
}

View File

@@ -21,8 +21,11 @@ import static com.android.documentsui.model.Document.asFileNotFoundException;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.DocumentsContract.DocumentRoot;
import android.util.Log;
import com.android.documentsui.RootsCache;
import org.json.JSONArray;
import org.json.JSONException;
@@ -62,4 +65,18 @@ public class DocumentStack extends LinkedList<Document> {
// TODO: handle roots that have gone missing
return stack;
}
public DocumentRoot getRoot(RootsCache roots) {
return roots.findRoot(getLast().uri);
}
public String getTitle(RootsCache roots) {
if (size() == 1) {
return getRoot(roots).title;
} else if (size() > 1) {
return peek().displayName;
} else {
return null;
}
}
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.model;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
import android.provider.DocumentsContract;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import com.android.documentsui.DocumentsActivity;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.List;
/**
* Representation of a storage backend.
*/
public class DocumentsProviderInfo {
private static final String TAG = DocumentsActivity.TAG;
public ProviderInfo providerInfo;
public boolean customRoots;
public List<Icon> customIcons;
public static class Icon {
public String mimeType;
public Drawable icon;
}
private static final String TAG_DOCUMENTS_PROVIDER = "documents-provider";
private static final String TAG_ICON = "icon";
public static DocumentsProviderInfo buildRecents(Context context, ProviderInfo providerInfo) {
final DocumentsProviderInfo info = new DocumentsProviderInfo();
info.providerInfo = providerInfo;
info.customRoots = false;
return info;
}
public static DocumentsProviderInfo parseInfo(Context context, ProviderInfo providerInfo) {
final DocumentsProviderInfo info = new DocumentsProviderInfo();
info.providerInfo = providerInfo;
info.customIcons = Lists.newArrayList();
final PackageManager pm = context.getPackageManager();
final Resources res;
try {
res = pm.getResourcesForApplication(providerInfo.applicationInfo);
} catch (NameNotFoundException e) {
Log.w(TAG, "Failed to find resources for " + providerInfo, e);
return null;
}
XmlResourceParser parser = null;
try {
parser = providerInfo.loadXmlMetaData(
pm, DocumentsContract.META_DATA_DOCUMENT_PROVIDER);
AttributeSet attrs = Xml.asAttributeSet(parser);
int type = 0;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
final String tag = parser.getName();
if (type == XmlPullParser.START_TAG && TAG_DOCUMENTS_PROVIDER.equals(tag)) {
final TypedArray a = res.obtainAttributes(
attrs, com.android.internal.R.styleable.DocumentsProviderInfo);
info.customRoots = a.getBoolean(
com.android.internal.R.styleable.DocumentsProviderInfo_customRoots,
false);
a.recycle();
} else if (type == XmlPullParser.START_TAG && TAG_ICON.equals(tag)) {
final TypedArray a = res.obtainAttributes(
attrs, com.android.internal.R.styleable.Icon);
final Icon icon = new Icon();
icon.mimeType = a.getString(com.android.internal.R.styleable.Icon_mimeType);
icon.icon = a.getDrawable(com.android.internal.R.styleable.Icon_icon);
info.customIcons.add(icon);
a.recycle();
}
}
} catch (IOException e) {
Log.w(TAG, "Failed to parse metadata", e);
return null;
} catch (XmlPullParserException e) {
Log.w(TAG, "Failed to parse metadata", e);
return null;
} finally {
IoUtils.closeQuietly(parser);
}
return info;
}
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.model;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Documents;
import android.provider.DocumentsContract.RootColumns;
import android.provider.DocumentsContract.Roots;
import com.android.documentsui.R;
import java.util.Comparator;
/**
* Representation of a root under a storage backend.
*/
public class Root {
public String rootId;
public int rootType;
public Uri uri;
public Drawable icon;
public String title;
public String summary;
public long availableBytes = -1;
public boolean isRecents;
public static Root buildRecents(Context context) {
final PackageManager pm = context.getPackageManager();
final Root root = new Root();
root.rootId = null;
root.rootType = Roots.ROOT_TYPE_SHORTCUT;
root.uri = null;
root.icon = context.getResources().getDrawable(R.drawable.ic_dir);
root.title = context.getString(R.string.root_recent);
root.summary = null;
root.availableBytes = -1;
root.isRecents = true;
return root;
}
public static Root fromCursor(
Context context, DocumentsProviderInfo info, Cursor cursor) {
final PackageManager pm = context.getPackageManager();
final Root root = new Root();
root.rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
root.uri = DocumentsContract.buildDocumentUri(
info.providerInfo.authority, root.rootId, Documents.DOC_ID_ROOT);
root.icon = info.providerInfo.loadIcon(pm);
root.title = info.providerInfo.loadLabel(pm).toString();
root.availableBytes = cursor.getLong(cursor.getColumnIndex(RootColumns.AVAILABLE_BYTES));
root.summary = null;
final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
if (icon != 0) {
try {
root.icon = pm.getResourcesForApplication(info.providerInfo.applicationInfo)
.getDrawable(icon);
} catch (NotFoundException e) {
throw new RuntimeException(e);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
}
final String title = cursor.getString(cursor.getColumnIndex(RootColumns.TITLE));
if (title != null) {
root.title = title;
}
root.summary = cursor.getString(cursor.getColumnIndex(RootColumns.SUMMARY));
root.isRecents = false;
return root;
}
/**
* Return string most suited to showing in a directory listing.
*/
public String getDirectoryString() {
return (summary != null) ? summary : title;
}
public static class RootComparator implements Comparator<Root> {
@Override
public int compare(Root lhs, Root rhs) {
final int score = compareToIgnoreCaseNullable(lhs.title, rhs.title);
if (score != 0) {
return score;
} else {
return compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
}
}
}
public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
if (lhs == null) return -1;
if (rhs == null) return 1;
return lhs.compareToIgnoreCase(rhs);
}
}

View File

@@ -15,18 +15,5 @@
android:name="android.content.DOCUMENT_PROVIDER"
android:resource="@xml/document_provider" />
</provider>
<!-- TODO: remove when we have real providers -->
<provider
android:name=".CloudTestDocumentsProvider"
android:authorities="com.android.externalstorage.cloudtest"
android:grantUriPermissions="true"
android:exported="true"
android:enabled="false"
android:permission="android.permission.MANAGE_DOCUMENTS">
<meta-data
android:name="android.content.DOCUMENT_PROVIDER"
android:resource="@xml/document_provider" />
</provider>
</application>
</manifest>

View File

@@ -1,253 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.externalstorage;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract.Documents;
import android.provider.DocumentsContract.RootColumns;
import android.provider.DocumentsContract.Roots;
import android.util.Log;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
import java.io.FileNotFoundException;
import java.util.List;
public class CloudTestDocumentsProvider extends ContentProvider {
private static final String TAG = "CloudTest";
private static final String AUTHORITY = "com.android.externalstorage.cloudtest";
private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int URI_ROOTS = 1;
private static final int URI_ROOTS_ID = 2;
private static final int URI_DOCS_ID = 3;
private static final int URI_DOCS_ID_CONTENTS = 4;
private static final int URI_DOCS_ID_SEARCH = 5;
static {
sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
}
private static final String[] ALL_ROOTS_COLUMNS = new String[] {
RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
};
private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
};
private List<String> mKnownDocs = Lists.newArrayList("meow.png", "kittens.pdf");
private int mPage;
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
switch (sMatcher.match(uri)) {
case URI_ROOTS: {
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_ROOTS_COLUMNS);
includeDefaultRoot(result);
return result;
}
case URI_ROOTS_ID: {
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_ROOTS_COLUMNS);
includeDefaultRoot(result);
return result;
}
case URI_DOCS_ID: {
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
includeDoc(result, docId);
return result;
}
case URI_DOCS_ID_CONTENTS: {
final CloudCursor result = new CloudCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS, uri);
for (String docId : mKnownDocs) {
includeDoc(result, docId);
}
if (mPage < 3) {
result.setHasMore();
}
result.setNotificationUri(getContext().getContentResolver(), uri);
return result;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
private void includeDefaultRoot(MatrixCursor result) {
final RowBuilder row = result.newRow();
row.offer(RootColumns.ROOT_ID, "testroot");
row.offer(RootColumns.ROOT_TYPE, Roots.ROOT_TYPE_SERVICE);
row.offer(RootColumns.TITLE, "_TestTitle");
row.offer(RootColumns.SUMMARY, "_TestSummary");
}
private void includeDoc(MatrixCursor result, String docId) {
int flags = 0;
final String mimeType;
if (Documents.DOC_ID_ROOT.equals(docId)) {
mimeType = Documents.MIME_TYPE_DIR;
} else {
mimeType = "application/octet-stream";
}
final RowBuilder row = result.newRow();
row.offer(DocumentColumns.DOC_ID, docId);
row.offer(DocumentColumns.DISPLAY_NAME, docId);
row.offer(DocumentColumns.MIME_TYPE, mimeType);
row.offer(DocumentColumns.LAST_MODIFIED, System.currentTimeMillis());
row.offer(DocumentColumns.FLAGS, flags);
}
private class CloudCursor extends MatrixCursor {
private final Uri mUri;
private Bundle mExtras = new Bundle();
public CloudCursor(String[] columnNames, Uri uri) {
super(columnNames);
mUri = uri;
}
public void setHasMore() {
mExtras.putBoolean(DocumentsContract.EXTRA_HAS_MORE, true);
}
@Override
public Bundle getExtras() {
Log.d(TAG, "getExtras() " + mExtras);
return mExtras;
}
@Override
public Bundle respond(Bundle extras) {
extras.size();
Log.d(TAG, "respond() " + extras);
if (extras.getBoolean(DocumentsContract.EXTRA_REQUEST_MORE, false)) {
new CloudTask().execute(mUri);
}
return Bundle.EMPTY;
}
}
private class CloudTask extends AsyncTask<Uri, Void, Void> {
@Override
protected Void doInBackground(Uri... uris) {
final Uri uri = uris[0];
SystemClock.sleep(1000);
// Grab some files from the cloud
for (int i = 0; i < 5; i++) {
mKnownDocs.add("cloud-page" + mPage + "-file" + i);
}
mPage++;
Log.d(TAG, "Loaded more; notifying " + uri);
getContext().getContentResolver().notifyChange(uri, null, false);
return null;
}
}
private interface TypeQuery {
final String[] PROJECTION = {
DocumentColumns.MIME_TYPE };
final int MIME_TYPE = 0;
}
@Override
public String getType(Uri uri) {
switch (sMatcher.match(uri)) {
case URI_ROOTS: {
return Roots.MIME_TYPE_DIR;
}
case URI_ROOTS_ID: {
return Roots.MIME_TYPE_ITEM;
}
case URI_DOCS_ID: {
final Cursor cursor = query(uri, TypeQuery.PROJECTION, null, null, null);
try {
if (cursor.moveToFirst()) {
return cursor.getString(TypeQuery.MIME_TYPE);
} else {
return null;
}
} finally {
IoUtils.closeQuietly(cursor);
}
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}

View File

@@ -16,205 +16,130 @@
package com.android.externalstorage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.graphics.Point;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract.DocumentRoot;
import android.provider.DocumentsContract.Documents;
import android.provider.DocumentsContract.RootColumns;
import android.provider.DocumentsContract.Roots;
import android.util.Log;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class ExternalStorageProvider extends ContentProvider {
public class ExternalStorageProvider extends DocumentsProvider {
private static final String TAG = "ExternalStorage";
private static final String AUTHORITY = "com.android.externalstorage.documents";
// docId format: root:path/to/file
// TODO: support multiple storage devices
private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int URI_ROOTS = 1;
private static final int URI_ROOTS_ID = 2;
private static final int URI_DOCS_ID = 3;
private static final int URI_DOCS_ID_CONTENTS = 4;
private static final int URI_DOCS_ID_SEARCH = 5;
static {
sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
}
private HashMap<String, Root> mRoots = Maps.newHashMap();
private static class Root {
public int rootType;
public String name;
public int icon = 0;
public String title = null;
public String summary = null;
public File path;
}
private static final String[] ALL_ROOTS_COLUMNS = new String[] {
RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
};
private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
private static final String[] SUPPORTED_COLUMNS = new String[] {
DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
};
private ArrayList<DocumentRoot> mRoots;
private HashMap<String, DocumentRoot> mTagToRoot;
private HashMap<String, File> mTagToPath;
@Override
public boolean onCreate() {
mRoots.clear();
mRoots = Lists.newArrayList();
mTagToRoot = Maps.newHashMap();
mTagToPath = Maps.newHashMap();
final Root root = new Root();
root.rootType = Roots.ROOT_TYPE_DEVICE_ADVANCED;
root.name = "primary";
root.title = getContext().getString(R.string.root_internal_storage);
root.path = Environment.getExternalStorageDirectory();
mRoots.put(root.name, root);
// TODO: support multiple storage devices
try {
final String tag = "primary";
final File path = Environment.getExternalStorageDirectory();
mTagToPath.put(tag, path);
final DocumentRoot root = new DocumentRoot();
root.docId = getDocIdForFile(path);
root.rootType = DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED;
root.title = getContext().getString(R.string.root_internal_storage);
root.icon = R.drawable.ic_pdf;
root.flags = DocumentRoot.FLAG_LOCAL_ONLY;
mRoots.add(root);
mTagToRoot.put(tag, root);
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
switch (sMatcher.match(uri)) {
case URI_ROOTS: {
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_ROOTS_COLUMNS);
for (Root root : mRoots.values()) {
includeRoot(result, root);
}
return result;
}
case URI_ROOTS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
private String getDocIdForFile(File file) throws FileNotFoundException {
String path = file.getAbsolutePath();
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_ROOTS_COLUMNS);
includeRoot(result, root);
return result;
}
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
final File file = docIdToFile(root, docId);
includeFile(result, root, file);
return result;
}
case URI_DOCS_ID_CONTENTS: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
final File parent = docIdToFile(root, docId);
for (File file : parent.listFiles()) {
includeFile(result, root, file);
}
return result;
}
case URI_DOCS_ID_SEARCH: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final String query = DocumentsContract.getSearchQuery(uri).toLowerCase();
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
final File parent = docIdToFile(root, docId);
final LinkedList<File> pending = new LinkedList<File>();
pending.add(parent);
while (!pending.isEmpty() && result.getCount() < 20) {
final File file = pending.removeFirst();
if (file.isDirectory()) {
for (File child : file.listFiles()) {
pending.add(child);
}
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, root, file);
}
}
}
return result;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mTagToPath.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
}
private String fileToDocId(Root root, File file) {
String rootPath = root.path.getAbsolutePath();
final String path = file.getAbsolutePath();
if (path.equals(rootPath)) {
return Documents.DOC_ID_ROOT;
if (mostSpecific == null) {
throw new FileNotFoundException("Failed to find root that contains " + path);
}
if (!rootPath.endsWith("/")) {
rootPath += "/";
}
if (!path.startsWith(rootPath)) {
throw new IllegalArgumentException("File " + path + " outside root " + root.path);
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.equals(path)) {
path = "";
} else if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
return path.substring(rootPath.length());
path = path.substring(rootPath.length() + 1);
}
return mostSpecific.getKey() + ':' + path;
}
private File docIdToFile(Root root, String docId) {
if (Documents.DOC_ID_ROOT.equals(docId)) {
return root.path;
private File getFileForDocId(String docId) throws FileNotFoundException {
final int splitIndex = docId.indexOf(':', 1);
final String tag = docId.substring(0, splitIndex);
final String path = docId.substring(splitIndex + 1);
File target = mTagToPath.get(tag);
if (target == null) {
throw new FileNotFoundException("No root for " + tag);
}
target = new File(target, path);
if (!target.exists()) {
throw new FileNotFoundException("Missing file for " + docId + " at " + target);
}
return target;
}
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
return new File(root.path, docId);
file = getFileForDocId(docId);
}
}
private void includeRoot(MatrixCursor result, Root root) {
final RowBuilder row = result.newRow();
row.offer(RootColumns.ROOT_ID, root.name);
row.offer(RootColumns.ROOT_TYPE, root.rootType);
row.offer(RootColumns.ICON, root.icon);
row.offer(RootColumns.TITLE, root.title);
row.offer(RootColumns.SUMMARY, root.summary);
row.offer(RootColumns.AVAILABLE_BYTES, root.path.getFreeSpace());
}
private void includeFile(MatrixCursor result, Root root, File file) {
int flags = 0;
if (file.isDirectory()) {
@@ -229,19 +154,12 @@ public class ExternalStorageProvider extends ContentProvider {
flags |= Documents.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getTypeForFile(file);
if (mimeType.startsWith("image/")) {
flags |= Documents.FLAG_SUPPORTS_THUMBNAIL;
}
final String docId = fileToDocId(root, file);
final String displayName;
if (Documents.DOC_ID_ROOT.equals(docId)) {
displayName = root.title;
} else {
displayName = file.getName();
}
final RowBuilder row = result.newRow();
row.offer(DocumentColumns.DOC_ID, docId);
row.offer(DocumentColumns.DISPLAY_NAME, displayName);
@@ -252,26 +170,129 @@ public class ExternalStorageProvider extends ContentProvider {
}
@Override
public String getType(Uri uri) {
switch (sMatcher.match(uri)) {
case URI_ROOTS: {
return Roots.MIME_TYPE_DIR;
public List<DocumentRoot> getDocumentRoots() {
// Update free space
for (String tag : mTagToRoot.keySet()) {
final DocumentRoot root = mTagToRoot.get(tag);
final File path = mTagToPath.get(tag);
root.availableBytes = path.getFreeSpace();
}
return mRoots;
}
@Override
public String createDocument(String docId, String mimeType, String displayName)
throws FileNotFoundException {
final File parent = getFileForDocId(docId);
displayName = validateDisplayName(mimeType, displayName);
final File file = new File(parent, displayName);
if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
if (!file.mkdir()) {
throw new IllegalStateException("Failed to mkdir " + file);
}
case URI_ROOTS_ID: {
return Roots.MIME_TYPE_ITEM;
}
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
return getTypeForFile(docIdToFile(root, docId));
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
} else {
try {
if (!file.createNewFile()) {
throw new IllegalStateException("Failed to touch " + file);
}
} catch (IOException e) {
throw new IllegalStateException("Failed to touch " + file + ": " + e);
}
}
return getDocIdForFile(file);
}
@Override
public void renameDocument(String docId, String displayName) throws FileNotFoundException {
final File file = getFileForDocId(docId);
final File newFile = new File(file.getParentFile(), displayName);
if (!file.renameTo(newFile)) {
throw new IllegalStateException("Failed to rename " + docId);
}
// TODO: update any outstanding grants
}
@Override
public void deleteDocument(String docId) throws FileNotFoundException {
final File file = getFileForDocId(docId);
if (!file.delete()) {
throw new IllegalStateException("Failed to delete " + file);
}
}
private String getTypeForFile(File file) {
@Override
public Cursor queryDocument(String docId) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
includeFile(result, docId, null);
return result;
}
@Override
public Cursor queryDocumentChildren(String docId) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
final File parent = getFileForDocId(docId);
for (File file : parent.listFiles()) {
includeFile(result, null, file);
}
return result;
}
@Override
public Cursor querySearch(String docId, String query) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
final File parent = getFileForDocId(docId);
final LinkedList<File> pending = new LinkedList<File>();
pending.add(parent);
while (!pending.isEmpty() && result.getCount() < 20) {
final File file = pending.removeFirst();
if (file.isDirectory()) {
for (File child : file.listFiles()) {
pending.add(child);
}
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
return result;
}
@Override
public String getType(String docId) throws FileNotFoundException {
final File file = getFileForDocId(docId);
return getTypeForFile(file);
}
@Override
public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
throws FileNotFoundException {
final File file = getFileForDocId(docId);
return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(null, mode));
}
@Override
public AssetFileDescriptor openDocumentThumbnail(
String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(docId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
file, ParcelFileDescriptor.MODE_READ_ONLY);
try {
final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
final long[] thumb = exif.getThumbnailRange();
if (thumb != null) {
return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
}
} catch (IOException e) {
}
return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}
private static String getTypeForFile(File file) {
if (file.isDirectory()) {
return Documents.MIME_TYPE_DIR;
} else {
@@ -279,7 +300,7 @@ public class ExternalStorageProvider extends ContentProvider {
}
}
private String getTypeForName(String name) {
private static String getTypeForName(String name) {
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
@@ -292,129 +313,7 @@ public class ExternalStorageProvider extends ContentProvider {
return "application/octet-stream";
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File file = docIdToFile(root, docId);
return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
if (opts == null || !opts.containsKey(DocumentsContract.EXTRA_THUMBNAIL_SIZE)) {
return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
}
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File file = docIdToFile(root, docId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
file, ParcelFileDescriptor.MODE_READ_ONLY);
try {
final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
final long[] thumb = exif.getThumbnailRange();
if (thumb != null) {
return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
}
} catch (IOException e) {
}
return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File parent = docIdToFile(root, docId);
final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
final String name = validateDisplayName(
values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType);
final File file = new File(parent, name);
if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
if (!file.mkdir()) {
return null;
}
} else {
try {
if (!file.createNewFile()) {
return null;
}
} catch (IOException e) {
Log.w(TAG, "Failed to create file", e);
return null;
}
}
final String newDocId = fileToDocId(root, file);
return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File file = docIdToFile(root, docId);
final File newFile = new File(
file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
return file.renameTo(newFile) ? 1 : 0;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final File file = docIdToFile(root, docId);
return file.delete() ? 1 : 0;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
}
private String validateDisplayName(String displayName, String mimeType) {
private static String validateDisplayName(String mimeType, String displayName) {
if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
return displayName;
} else {