diff options
Diffstat (limited to 'core/java/android/provider/DocumentsProvider.java')
-rw-r--r-- | core/java/android/provider/DocumentsProvider.java | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java new file mode 100644 index 0000000..eeb8c41 --- /dev/null +++ b/core/java/android/provider/DocumentsProvider.java @@ -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); + } +} |