blob: eeb8c41435ac1801b169d9aa0ab20923ed24c679 [file] [log] [blame]
/*
* 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);
}
}