/* * 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. *

* 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. *

* 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. *

* 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 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 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); } }