Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.provider;
     18 
     19 import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE;
     20 import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
     21 import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
     22 import static android.provider.DocumentsContract.getDocumentId;
     23 import static android.provider.DocumentsContract.getRootId;
     24 import static android.provider.DocumentsContract.getSearchDocumentsQuery;
     25 
     26 import android.content.ContentProvider;
     27 import android.content.ContentResolver;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.UriMatcher;
     32 import android.content.pm.PackageManager;
     33 import android.content.pm.ProviderInfo;
     34 import android.content.res.AssetFileDescriptor;
     35 import android.database.Cursor;
     36 import android.graphics.Point;
     37 import android.net.Uri;
     38 import android.os.Bundle;
     39 import android.os.CancellationSignal;
     40 import android.os.ParcelFileDescriptor;
     41 import android.os.ParcelFileDescriptor.OnCloseListener;
     42 import android.provider.DocumentsContract.Document;
     43 import android.provider.DocumentsContract.Root;
     44 import android.util.Log;
     45 
     46 import libcore.io.IoUtils;
     47 
     48 import java.io.FileNotFoundException;
     49 
     50 /**
     51  * Base class for a document provider. A document provider offers read and write
     52  * access to durable files, such as files stored on a local disk, or files in a
     53  * cloud storage service. To create a document provider, extend this class,
     54  * implement the abstract methods, and add it to your manifest like this:
     55  *
     56  * <pre class="prettyprint">&lt;manifest&gt;
     57  *    ...
     58  *    &lt;application&gt;
     59  *        ...
     60  *        &lt;provider
     61  *            android:name="com.example.MyCloudProvider"
     62  *            android:authorities="com.example.mycloudprovider"
     63  *            android:exported="true"
     64  *            android:grantUriPermissions="true"
     65  *            android:permission="android.permission.MANAGE_DOCUMENTS"
     66  *            android:enabled="@bool/isAtLeastKitKat"&gt;
     67  *            &lt;intent-filter&gt;
     68  *                &lt;action android:name="android.content.action.DOCUMENTS_PROVIDER" /&gt;
     69  *            &lt;/intent-filter&gt;
     70  *        &lt;/provider&gt;
     71  *        ...
     72  *    &lt;/application&gt;
     73  *&lt;/manifest&gt;</pre>
     74  * <p>
     75  * When defining your provider, you must protect it with
     76  * {@link android.Manifest.permission#MANAGE_DOCUMENTS}, which is a permission
     77  * only the system can obtain. Applications cannot use a documents provider
     78  * directly; they must go through {@link Intent#ACTION_OPEN_DOCUMENT} or
     79  * {@link Intent#ACTION_CREATE_DOCUMENT} which requires a user to actively
     80  * navigate and select documents. When a user selects documents through that UI,
     81  * the system issues narrow URI permission grants to the requesting application.
     82  * </p>
     83  * <h3>Documents</h3>
     84  * <p>
     85  * A document can be either an openable stream (with a specific MIME type), or a
     86  * directory containing additional documents (with the
     87  * {@link Document#MIME_TYPE_DIR} MIME type). Each directory represents the top
     88  * of a subtree containing zero or more documents, which can recursively contain
     89  * even more documents and directories.
     90  * </p>
     91  * <p>
     92  * Each document can have different capabilities, as described by
     93  * {@link Document#COLUMN_FLAGS}. For example, if a document can be represented
     94  * as a thumbnail, your provider can set
     95  * {@link Document#FLAG_SUPPORTS_THUMBNAIL} and implement
     96  * {@link #openDocumentThumbnail(String, Point, CancellationSignal)} to return
     97  * that thumbnail.
     98  * </p>
     99  * <p>
    100  * Each document under a provider is uniquely referenced by its
    101  * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. A
    102  * single document can be included in multiple directories when responding to
    103  * {@link #queryChildDocuments(String, String[], String)}. For example, a
    104  * provider might surface a single photo in multiple locations: once in a
    105  * directory of geographic locations, and again in a directory of dates.
    106  * </p>
    107  * <h3>Roots</h3>
    108  * <p>
    109  * All documents are surfaced through one or more "roots." Each root represents
    110  * the top of a document tree that a user can navigate. For example, a root
    111  * could represent an account or a physical storage device. Similar to
    112  * documents, each root can have capabilities expressed through
    113  * {@link Root#COLUMN_FLAGS}.
    114  * </p>
    115  *
    116  * @see Intent#ACTION_OPEN_DOCUMENT
    117  * @see Intent#ACTION_CREATE_DOCUMENT
    118  */
    119 public abstract class DocumentsProvider extends ContentProvider {
    120     private static final String TAG = "DocumentsProvider";
    121 
    122     private static final int MATCH_ROOTS = 1;
    123     private static final int MATCH_ROOT = 2;
    124     private static final int MATCH_RECENT = 3;
    125     private static final int MATCH_SEARCH = 4;
    126     private static final int MATCH_DOCUMENT = 5;
    127     private static final int MATCH_CHILDREN = 6;
    128 
    129     private String mAuthority;
    130 
    131     private UriMatcher mMatcher;
    132 
    133     /**
    134      * Implementation is provided by the parent class.
    135      */
    136     @Override
    137     public void attachInfo(Context context, ProviderInfo info) {
    138         mAuthority = info.authority;
    139 
    140         mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    141         mMatcher.addURI(mAuthority, "root", MATCH_ROOTS);
    142         mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT);
    143         mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT);
    144         mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
    145         mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
    146         mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
    147 
    148         // Sanity check our setup
    149         if (!info.exported) {
    150             throw new SecurityException("Provider must be exported");
    151         }
    152         if (!info.grantUriPermissions) {
    153             throw new SecurityException("Provider must grantUriPermissions");
    154         }
    155         if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
    156                 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
    157             throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
    158         }
    159 
    160         super.attachInfo(context, info);
    161     }
    162 
    163     /**
    164      * Create a new document and return its newly generated
    165      * {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new
    166      * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must
    167      * not change once returned.
    168      *
    169      * @param parentDocumentId the parent directory to create the new document
    170      *            under.
    171      * @param mimeType the concrete MIME type associated with the new document.
    172      *            If the MIME type is not supported, the provider must throw.
    173      * @param displayName the display name of the new document. The provider may
    174      *            alter this name to meet any internal constraints, such as
    175      *            conflicting names.
    176      */
    177     @SuppressWarnings("unused")
    178     public String createDocument(String parentDocumentId, String mimeType, String displayName)
    179             throws FileNotFoundException {
    180         throw new UnsupportedOperationException("Create not supported");
    181     }
    182 
    183     /**
    184      * Delete the requested document. Upon returning, any URI permission grants
    185      * for the requested document will be revoked. If additional documents were
    186      * deleted as a side effect of this call, such as documents inside a
    187      * directory, the implementor is responsible for revoking those permissions.
    188      *
    189      * @param documentId the document to delete.
    190      */
    191     @SuppressWarnings("unused")
    192     public void deleteDocument(String documentId) throws FileNotFoundException {
    193         throw new UnsupportedOperationException("Delete not supported");
    194     }
    195 
    196     /**
    197      * Return all roots currently provided. To display to users, you must define
    198      * at least one root. You should avoid making network requests to keep this
    199      * request fast.
    200      * <p>
    201      * Each root is defined by the metadata columns described in {@link Root},
    202      * including {@link Root#COLUMN_DOCUMENT_ID} which points to a directory
    203      * representing a tree of documents to display under that root.
    204      * <p>
    205      * If this set of roots changes, you must call {@link ContentResolver#notifyChange(Uri,
    206      * android.database.ContentObserver, boolean)} with
    207      * {@link DocumentsContract#buildRootsUri(String)} to notify the system.
    208      *
    209      * @param projection list of {@link Root} columns to put into the cursor. If
    210      *            {@code null} all supported columns should be included.
    211      */
    212     public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException;
    213 
    214     /**
    215      * Return recently modified documents under the requested root. This will
    216      * only be called for roots that advertise
    217      * {@link Root#FLAG_SUPPORTS_RECENTS}. The returned documents should be
    218      * sorted by {@link Document#COLUMN_LAST_MODIFIED} in descending order, and
    219      * limited to only return the 64 most recently modified documents.
    220      * <p>
    221      * Recent documents do not support change notifications.
    222      *
    223      * @param projection list of {@link Document} columns to put into the
    224      *            cursor. If {@code null} all supported columns should be
    225      *            included.
    226      * @see DocumentsContract#EXTRA_LOADING
    227      */
    228     @SuppressWarnings("unused")
    229     public Cursor queryRecentDocuments(String rootId, String[] projection)
    230             throws FileNotFoundException {
    231         throw new UnsupportedOperationException("Recent not supported");
    232     }
    233 
    234     /**
    235      * Return metadata for the single requested document. You should avoid
    236      * making network requests to keep this request fast.
    237      *
    238      * @param documentId the document to return.
    239      * @param projection list of {@link Document} columns to put into the
    240      *            cursor. If {@code null} all supported columns should be
    241      *            included.
    242      */
    243     public abstract Cursor queryDocument(String documentId, String[] projection)
    244             throws FileNotFoundException;
    245 
    246     /**
    247      * Return the children documents contained in the requested directory. This
    248      * must only return immediate descendants, as additional queries will be
    249      * issued to recursively explore the tree.
    250      * <p>
    251      * If your provider is cloud-based, and you have some data cached or pinned
    252      * locally, you may return the local data immediately, setting
    253      * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that
    254      * you are still fetching additional data. Then, when the network data is
    255      * available, you can send a change notification to trigger a requery and
    256      * return the complete contents. To return a Cursor with extras, you need to
    257      * extend and override {@link Cursor#getExtras()}.
    258      * <p>
    259      * To support change notifications, you must
    260      * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant
    261      * Uri, such as
    262      * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then
    263      * you can call {@link ContentResolver#notifyChange(Uri,
    264      * android.database.ContentObserver, boolean)} with that Uri to send change
    265      * notifications.
    266      *
    267      * @param parentDocumentId the directory to return children for.
    268      * @param projection list of {@link Document} columns to put into the
    269      *            cursor. If {@code null} all supported columns should be
    270      *            included.
    271      * @param sortOrder how to order the rows, formatted as an SQL
    272      *            {@code ORDER BY} clause (excluding the ORDER BY itself).
    273      *            Passing {@code null} will use the default sort order, which
    274      *            may be unordered. This ordering is a hint that can be used to
    275      *            prioritize how data is fetched from the network, but UI may
    276      *            always enforce a specific ordering.
    277      * @see DocumentsContract#EXTRA_LOADING
    278      * @see DocumentsContract#EXTRA_INFO
    279      * @see DocumentsContract#EXTRA_ERROR
    280      */
    281     public abstract Cursor queryChildDocuments(
    282             String parentDocumentId, String[] projection, String sortOrder)
    283             throws FileNotFoundException;
    284 
    285     /** {@hide} */
    286     @SuppressWarnings("unused")
    287     public Cursor queryChildDocumentsForManage(
    288             String parentDocumentId, String[] projection, String sortOrder)
    289             throws FileNotFoundException {
    290         throw new UnsupportedOperationException("Manage not supported");
    291     }
    292 
    293     /**
    294      * Return documents that that match the given query under the requested
    295      * root. The returned documents should be sorted by relevance in descending
    296      * order. How documents are matched against the query string is an
    297      * implementation detail left to each provider, but it's suggested that at
    298      * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a
    299      * case-insensitive fashion.
    300      * <p>
    301      * Only documents may be returned; directories are not supported in search
    302      * results.
    303      * <p>
    304      * If your provider is cloud-based, and you have some data cached or pinned
    305      * locally, you may return the local data immediately, setting
    306      * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that
    307      * you are still fetching additional data. Then, when the network data is
    308      * available, you can send a change notification to trigger a requery and
    309      * return the complete contents.
    310      * <p>
    311      * To support change notifications, you must
    312      * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant
    313      * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String,
    314      * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri,
    315      * android.database.ContentObserver, boolean)} with that Uri to send change
    316      * notifications.
    317      *
    318      * @param rootId the root to search under.
    319      * @param query string to match documents against.
    320      * @param projection list of {@link Document} columns to put into the
    321      *            cursor. If {@code null} all supported columns should be
    322      *            included.
    323      * @see DocumentsContract#EXTRA_LOADING
    324      * @see DocumentsContract#EXTRA_INFO
    325      * @see DocumentsContract#EXTRA_ERROR
    326      */
    327     @SuppressWarnings("unused")
    328     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
    329             throws FileNotFoundException {
    330         throw new UnsupportedOperationException("Search not supported");
    331     }
    332 
    333     /**
    334      * Return concrete MIME type of the requested document. Must match the value
    335      * of {@link Document#COLUMN_MIME_TYPE} for this document. The default
    336      * implementation queries {@link #queryDocument(String, String[])}, so
    337      * providers may choose to override this as an optimization.
    338      */
    339     public String getDocumentType(String documentId) throws FileNotFoundException {
    340         final Cursor cursor = queryDocument(documentId, null);
    341         try {
    342             if (cursor.moveToFirst()) {
    343                 return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
    344             } else {
    345                 return null;
    346             }
    347         } finally {
    348             IoUtils.closeQuietly(cursor);
    349         }
    350     }
    351 
    352     /**
    353      * Open and return the requested document.
    354      * <p>
    355      * Your provider should return a reliable {@link ParcelFileDescriptor} to
    356      * detect when the remote caller has finished reading or writing the
    357      * document. You may return a pipe or socket pair if the mode is exclusively
    358      * "r" or "w", but complex modes like "rw" imply a normal file on disk that
    359      * supports seeking.
    360      * <p>
    361      * If you block while downloading content, you should periodically check
    362      * {@link CancellationSignal#isCanceled()} to abort abandoned open requests.
    363      *
    364      * @param documentId the document to return.
    365      * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
    366      * @param signal used by the caller to signal if the request should be
    367      *            cancelled. May be null.
    368      * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
    369      *      OnCloseListener)
    370      * @see ParcelFileDescriptor#createReliablePipe()
    371      * @see ParcelFileDescriptor#createReliableSocketPair()
    372      * @see ParcelFileDescriptor#parseMode(String)
    373      */
    374     public abstract ParcelFileDescriptor openDocument(
    375             String documentId, String mode, CancellationSignal signal) throws FileNotFoundException;
    376 
    377     /**
    378      * Open and return a thumbnail of the requested document.
    379      * <p>
    380      * A provider should return a thumbnail closely matching the hinted size,
    381      * attempting to serve from a local cache if possible. A provider should
    382      * never return images more than double the hinted size.
    383      * <p>
    384      * If you perform expensive operations to download or generate a thumbnail,
    385      * you should periodically check {@link CancellationSignal#isCanceled()} to
    386      * abort abandoned thumbnail requests.
    387      *
    388      * @param documentId the document to return.
    389      * @param sizeHint hint of the optimal thumbnail dimensions.
    390      * @param signal used by the caller to signal if the request should be
    391      *            cancelled. May be null.
    392      * @see Document#FLAG_SUPPORTS_THUMBNAIL
    393      */
    394     @SuppressWarnings("unused")
    395     public AssetFileDescriptor openDocumentThumbnail(
    396             String documentId, Point sizeHint, CancellationSignal signal)
    397             throws FileNotFoundException {
    398         throw new UnsupportedOperationException("Thumbnails not supported");
    399     }
    400 
    401     /**
    402      * Implementation is provided by the parent class. Cannot be overriden.
    403      *
    404      * @see #queryRoots(String[])
    405      * @see #queryRecentDocuments(String, String[])
    406      * @see #queryDocument(String, String[])
    407      * @see #queryChildDocuments(String, String[], String)
    408      * @see #querySearchDocuments(String, String, String[])
    409      */
    410     @Override
    411     public final Cursor query(Uri uri, String[] projection, String selection,
    412             String[] selectionArgs, String sortOrder) {
    413         try {
    414             switch (mMatcher.match(uri)) {
    415                 case MATCH_ROOTS:
    416                     return queryRoots(projection);
    417                 case MATCH_RECENT:
    418                     return queryRecentDocuments(getRootId(uri), projection);
    419                 case MATCH_SEARCH:
    420                     return querySearchDocuments(
    421                             getRootId(uri), getSearchDocumentsQuery(uri), projection);
    422                 case MATCH_DOCUMENT:
    423                     return queryDocument(getDocumentId(uri), projection);
    424                 case MATCH_CHILDREN:
    425                     if (DocumentsContract.isManageMode(uri)) {
    426                         return queryChildDocumentsForManage(
    427                                 getDocumentId(uri), projection, sortOrder);
    428                     } else {
    429                         return queryChildDocuments(getDocumentId(uri), projection, sortOrder);
    430                     }
    431                 default:
    432                     throw new UnsupportedOperationException("Unsupported Uri " + uri);
    433             }
    434         } catch (FileNotFoundException e) {
    435             Log.w(TAG, "Failed during query", e);
    436             return null;
    437         }
    438     }
    439 
    440     /**
    441      * Implementation is provided by the parent class. Cannot be overriden.
    442      *
    443      * @see #getDocumentType(String)
    444      */
    445     @Override
    446     public final String getType(Uri uri) {
    447         try {
    448             switch (mMatcher.match(uri)) {
    449                 case MATCH_ROOT:
    450                     return DocumentsContract.Root.MIME_TYPE_ITEM;
    451                 case MATCH_DOCUMENT:
    452                     return getDocumentType(getDocumentId(uri));
    453                 default:
    454                     return null;
    455             }
    456         } catch (FileNotFoundException e) {
    457             Log.w(TAG, "Failed during getType", e);
    458             return null;
    459         }
    460     }
    461 
    462     /**
    463      * Implementation is provided by the parent class. Throws by default, and
    464      * cannot be overriden.
    465      *
    466      * @see #createDocument(String, String, String)
    467      */
    468     @Override
    469     public final Uri insert(Uri uri, ContentValues values) {
    470         throw new UnsupportedOperationException("Insert not supported");
    471     }
    472 
    473     /**
    474      * Implementation is provided by the parent class. Throws by default, and
    475      * cannot be overriden.
    476      *
    477      * @see #deleteDocument(String)
    478      */
    479     @Override
    480     public final int delete(Uri uri, String selection, String[] selectionArgs) {
    481         throw new UnsupportedOperationException("Delete not supported");
    482     }
    483 
    484     /**
    485      * Implementation is provided by the parent class. Throws by default, and
    486      * cannot be overriden.
    487      */
    488     @Override
    489     public final int update(
    490             Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    491         throw new UnsupportedOperationException("Update not supported");
    492     }
    493 
    494     /**
    495      * Implementation is provided by the parent class. Can be overridden to
    496      * provide additional functionality, but subclasses <em>must</em> always
    497      * call the superclass. If the superclass returns {@code null}, the subclass
    498      * may implement custom behavior.
    499      *
    500      * @see #openDocument(String, String, CancellationSignal)
    501      * @see #deleteDocument(String)
    502      */
    503     @Override
    504     public Bundle call(String method, String arg, Bundle extras) {
    505         final Context context = getContext();
    506 
    507         if (!method.startsWith("android:")) {
    508             // Let non-platform methods pass through
    509             return super.call(method, arg, extras);
    510         }
    511 
    512         final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
    513         final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
    514 
    515         // Require that caller can manage requested document
    516         final boolean callerHasManage =
    517                 context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
    518                 == PackageManager.PERMISSION_GRANTED;
    519         enforceWritePermissionInner(documentUri);
    520 
    521         final Bundle out = new Bundle();
    522         try {
    523             if (METHOD_CREATE_DOCUMENT.equals(method)) {
    524                 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
    525                 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
    526 
    527                 final String newDocumentId = createDocument(documentId, mimeType, displayName);
    528                 out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);
    529 
    530                 // Extend permission grant towards caller if needed
    531                 if (!callerHasManage) {
    532                     final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
    533                             mAuthority, newDocumentId);
    534                     context.grantUriPermission(getCallingPackage(), newDocumentUri,
    535                             Intent.FLAG_GRANT_READ_URI_PERMISSION
    536                             | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    537                             | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    538                 }
    539 
    540             } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
    541                 deleteDocument(documentId);
    542 
    543                 // Document no longer exists, clean up any grants
    544                 context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
    545                         | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    546                         | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    547 
    548             } else {
    549                 throw new UnsupportedOperationException("Method not supported " + method);
    550             }
    551         } catch (FileNotFoundException e) {
    552             throw new IllegalStateException("Failed call " + method, e);
    553         }
    554         return out;
    555     }
    556 
    557     /**
    558      * Implementation is provided by the parent class. Cannot be overriden.
    559      *
    560      * @see #openDocument(String, String, CancellationSignal)
    561      */
    562     @Override
    563     public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    564         return openDocument(getDocumentId(uri), mode, null);
    565     }
    566 
    567     /**
    568      * Implementation is provided by the parent class. Cannot be overriden.
    569      *
    570      * @see #openDocument(String, String, CancellationSignal)
    571      */
    572     @Override
    573     public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
    574             throws FileNotFoundException {
    575         return openDocument(getDocumentId(uri), mode, signal);
    576     }
    577 
    578     /**
    579      * Implementation is provided by the parent class. Cannot be overriden.
    580      *
    581      * @see #openDocumentThumbnail(String, Point, CancellationSignal)
    582      */
    583     @Override
    584     public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
    585             throws FileNotFoundException {
    586         if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
    587             final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
    588             return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
    589         } else {
    590             return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
    591         }
    592     }
    593 
    594     /**
    595      * Implementation is provided by the parent class. Cannot be overriden.
    596      *
    597      * @see #openDocumentThumbnail(String, Point, CancellationSignal)
    598      */
    599     @Override
    600     public final AssetFileDescriptor openTypedAssetFile(
    601             Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
    602             throws FileNotFoundException {
    603         if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
    604             final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
    605             return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);
    606         } else {
    607             return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
    608         }
    609     }
    610 }
    611