/* * 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.net.TrafficStats.KB_IN_BYTES; import static libcore.io.OsConstants.SEEK_SET; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.res.AssetFileDescriptor; 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; import libcore.io.IoBridge; import libcore.io.IoUtils; import libcore.io.Libcore; import java.io.FileDescriptor; import java.io.IOException; import java.util.List; /** * Defines the contract between a documents provider and the platform. *

* To create a document provider, extend {@link DocumentsProvider}, which * provides a foundational implementation of this contract. * * @see DocumentsProvider */ public final class DocumentsContract { private static final String TAG = "Documents"; // content://com.example/docs/12/ // content://com.example/docs/12/children/ // content://com.example/docs/12/search/?query=pony private DocumentsContract() { } /** {@hide} */ public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER"; /** {@hide} */ public static final String ACTION_MANAGE_DOCUMENTS = "android.provider.action.MANAGE_DOCUMENTS"; /** {@hide} */ public static final String ACTION_DOCUMENT_ROOT_CHANGED = "android.provider.action.DOCUMENT_ROOT_CHANGED"; /** * Constants for individual documents. */ public final static class Documents { private Documents() { } /** * MIME type of a document which is a directory that may contain additional * documents. */ 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 */ public static final int FLAG_SUPPORTS_CREATE = 1; /** * Flag indicating that a document is renamable. * * @see DocumentColumns#FLAGS */ public static final int FLAG_SUPPORTS_RENAME = 1 << 1; /** * Flag indicating that a document is deletable. * * @see DocumentColumns#FLAGS */ public static final int FLAG_SUPPORTS_DELETE = 1 << 2; /** * Flag indicating that a document can be represented as a thumbnail. * * @see DocumentColumns#FLAGS */ public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3; /** * Flag indicating that a document is a directory that supports search. * * @see DocumentColumns#FLAGS */ public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; /** * Flag indicating that a document supports writing. * * @see DocumentColumns#FLAGS */ public static final int FLAG_SUPPORTS_WRITE = 1 << 5; /** * Flag indicating that a document is a directory that prefers its contents * be shown in a larger format grid. Usually suitable when a directory * contains mostly pictures. * * @see DocumentColumns#FLAGS */ public static final int FLAG_PREFERS_GRID = 1 << 6; } /** * Extra boolean flag included in a directory {@link Cursor#getExtras()} * indicating that a document provider is still loading data. For example, a * provider has returned some results, but is still waiting on an * outstanding network request. * * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, * boolean) */ public static final String EXTRA_LOADING = "loading"; /** * Extra string included in a directory {@link Cursor#getExtras()} * providing an informational message that should be shown to a user. For * example, a provider may wish to indicate that not all documents are * available. */ public static final String EXTRA_INFO = "info"; /** * Extra string included in a directory {@link Cursor#getExtras()} providing * an error message that should be shown to a user. For example, a provider * may wish to indicate that a network error occurred. The user may choose * to retry, resulting in a new query. */ public static final String EXTRA_ERROR = "error"; /** {@hide} */ public static final String METHOD_GET_ROOTS = "android:getRoots"; /** {@hide} */ public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; /** {@hide} */ public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument"; /** {@hide} */ public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; /** {@hide} */ public static final String EXTRA_AUTHORITY = "authority"; /** {@hide} */ public static final String EXTRA_PACKAGE_NAME = "packageName"; /** {@hide} */ public static final String EXTRA_URI = "uri"; /** {@hide} */ public static final String EXTRA_ROOTS = "roots"; /** {@hide} */ public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; private static final String PATH_DOCS = "docs"; private static final String PATH_CHILDREN = "children"; private static final String PATH_SEARCH = "search"; private static final String PARAM_QUERY = "query"; /** * Build Uri representing the given {@link DocumentColumns#DOC_ID} in a * document provider. */ public static Uri buildDocumentUri(String authority, String docId) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(authority).appendPath(PATH_DOCS).appendPath(docId).build(); } /** * Build Uri representing the contents of the given directory in a document * provider. The given document must be {@link Documents#MIME_TYPE_DIR}. * * @hide */ public static Uri buildChildrenUri(String authority, String docId) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_CHILDREN).build(); } /** * Build Uri representing a search for matching documents under a specific * directory in a document provider. The given document must have * {@link Documents#FLAG_SUPPORTS_SEARCH}. * * @hide */ public static Uri buildSearchUri(String authority, String docId, String query) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_SEARCH) .appendQueryParameter(PARAM_QUERY, query).build(); } /** * Extract the {@link DocumentColumns#DOC_ID} from the given Uri. */ public static String getDocId(Uri documentUri) { final List paths = documentUri.getPathSegments(); if (paths.size() < 2) { throw new IllegalArgumentException("Not a document: " + documentUri); } if (!PATH_DOCS.equals(paths.get(0))) { throw new IllegalArgumentException("Not a document: " + documentUri); } return paths.get(1); } /** {@hide} */ public static String getSearchQuery(Uri documentUri) { return documentUri.getQueryParameter(PARAM_QUERY); } /** * Standard columns for document queries. Document providers must * support at least these columns when queried. */ public interface DocumentColumns extends OpenableColumns { /** * Unique ID for a document. Values must never change once * returned, since they may used for long-term Uri permission grants. *

* Type: STRING */ public static final String DOC_ID = "doc_id"; /** * MIME type of a document. *

* Type: STRING * * @see Documents#MIME_TYPE_DIR */ public static final String MIME_TYPE = "mime_type"; /** * Timestamp when a document was last modified, in milliseconds since * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. Document * providers can update this field using events from * {@link OnCloseListener} or other reliable * {@link ParcelFileDescriptor} transports. *

* Type: INTEGER (long) * * @see System#currentTimeMillis() */ public static final String LAST_MODIFIED = "last_modified"; /** * Specific icon resource for a document, or {@code null} to resolve * default using {@link #MIME_TYPE}. *

* Type: INTEGER (int) */ public static final String ICON = "icon"; /** * Summary for a document, or {@code null} to omit. *

* Type: STRING */ public static final String SUMMARY = "summary"; /** * Flags that apply to a specific document. *

* Type: INTEGER (int) */ public static final String FLAGS = "flags"; } /** * Metadata about a specific root of documents. */ public final static class DocumentRoot implements Parcelable { /** * Root that represents a storage service, such as a cloud-based * service. * * @see #rootType */ public static final int ROOT_TYPE_SERVICE = 1; /** * Root that represents a shortcut to content that may be available * elsewhere through another storage root. * * @see #rootType */ public static final int ROOT_TYPE_SHORTCUT = 2; /** * Root that represents a physical storage device. * * @see #rootType */ public static final int ROOT_TYPE_DEVICE = 3; /** * Root that represents a physical storage device that should only be * displayed to advanced users. * * @see #rootType */ public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; /** * Flag indicating that at least one directory under this root supports * creating content. * * @see #flags */ public static final int FLAG_SUPPORTS_CREATE = 1; /** * 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 int FLAG_LOCAL_ONLY = 1 << 1; /** {@hide} */ public String authority; /** * Root type, use for clustering. * * @see #ROOT_TYPE_SERVICE * @see #ROOT_TYPE_DEVICE */ public int rootType; /** * Flags for this root. * * @see #FLAG_LOCAL_ONLY */ public int flags; /** * Icon resource ID for this root. */ 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 CREATOR = new Creator() { @Override public DocumentRoot createFromParcel(Parcel in) { return new DocumentRoot(in); } @Override public DocumentRoot[] newArray(int size) { return new DocumentRoot[size]; } }; } /** * Return list of all documents that the calling package has "open." These * are Uris matching {@link DocumentsContract} to which persistent * read/write access has been granted, usually through * {@link Intent#ACTION_OPEN_DOCUMENT} or * {@link Intent#ACTION_CREATE_DOCUMENT}. * * @see Context#grantUriPermission(String, Uri, int) * @see ContentResolver#getIncomingUriPermissionGrants(int, int) */ public static Uri[] getOpenDocuments(Context context) { final int openedFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION; final Uri[] uris = context.getContentResolver() .getIncomingUriPermissionGrants(openedFlags, openedFlags); // Filter to only include document providers final PackageManager pm = context.getPackageManager(); final List result = Lists.newArrayList(); for (Uri uri : uris) { final ProviderInfo info = pm.resolveContentProvider( uri.getAuthority(), PackageManager.GET_META_DATA); if (info.metaData.containsKey(META_DATA_DOCUMENT_PROVIDER)) { result.add(uri); } } return result.toArray(new Uri[result.size()]); } /** * Return thumbnail representing the document at the given URI. Callers are * responsible for their own in-memory caching. Given document must have * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set. * * @return 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(); openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size); AssetFileDescriptor afd = null; try { afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts); final FileDescriptor fd = afd.getFileDescriptor(); final long offset = afd.getStartOffset(); final long length = afd.getDeclaredLength(); // Some thumbnails might be a region inside a larger file, such as // an EXIF thumbnail. Since BitmapFactory aggressively seeks around // the entire file, we read the region manually. byte[] region = null; if (offset > 0 && length <= 64 * KB_IN_BYTES) { region = new byte[(int) length]; Libcore.os.lseek(fd, offset, SEEK_SET); if (IoBridge.read(fd, region, 0, region.length) != region.length) { region = null; } } // We requested a rough thumbnail size, but the remote size may have // returned something giant, so defensively scale down as needed. final BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; if (region != null) { BitmapFactory.decodeByteArray(region, 0, region.length, opts); } else { BitmapFactory.decodeFileDescriptor(fd, null, opts); } final int widthSample = opts.outWidth / size.x; final int heightSample = opts.outHeight / size.y; opts.inJustDecodeBounds = false; opts.inSampleSize = Math.min(widthSample, heightSample); Log.d(TAG, "Decoding with sample size " + opts.inSampleSize); if (region != null) { return BitmapFactory.decodeByteArray(region, 0, region.length, opts); } else { return BitmapFactory.decodeFileDescriptor(fd, null, opts); } } catch (ErrnoException e) { Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e); return null; } catch (IOException e) { Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e); return null; } finally { IoUtils.closeQuietly(afd); } } /** {@hide} */ public static List getDocumentRoots(ContentProviderClient client) { try { final Bundle out = client.call(METHOD_GET_ROOTS, null, null); final List roots = out.getParcelableArrayList(EXTRA_ROOTS); return roots; } catch (Exception e) { Log.w(TAG, "Failed to get roots", e); return null; } } /** * Create a new document under the given parent document with MIME type and * display name. * * @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 String createDocument( ContentProviderClient client, String docId, String mimeType, String displayName) { final Bundle in = new Bundle(); in.putString(DocumentColumns.DOC_ID, docId); in.putString(DocumentColumns.MIME_TYPE, mimeType); in.putString(DocumentColumns.DISPLAY_NAME, displayName); try { final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in); return out.getString(DocumentColumns.DOC_ID); } catch (Exception e) { Log.w(TAG, "Failed to create document", e); return null; } } /** * Rename the given document. * * @param docId document with {@link Documents#FLAG_SUPPORTS_RENAME} * @return document which may have changed due to rename, or {@code null} if * rename failed. * @hide */ public static String renameDocument( ContentProviderClient client, String docId, String displayName) { final Bundle in = new Bundle(); in.putString(DocumentColumns.DOC_ID, docId); in.putString(DocumentColumns.DISPLAY_NAME, displayName); try { final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in); return out.getString(DocumentColumns.DOC_ID); } catch (Exception e) { Log.w(TAG, "Failed to rename document", e); return null; } } /** * Delete the given document. * * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE} * @hide */ 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; } } }