am 9fd81a90: Merge "Stronger DocumentsProvider contract." into klp-dev
* commit '9fd81a9008d5c8dd33272b6a451d89fa2fa1841e': Stronger DocumentsProvider contract.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
384
core/java/android/provider/DocumentsProvider.java
Normal file
384
core/java/android/provider/DocumentsProvider.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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--) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user