DocumentsProvider.java revision b690b4de06385a821aed3442e10058986c03badc
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
17package android.provider;
18
19import static android.provider.DocumentsContract.METHOD_COPY_DOCUMENT;
20import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
21import static android.provider.DocumentsContract.METHOD_CREATE_WEB_LINK_INTENT;
22import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
23import static android.provider.DocumentsContract.METHOD_EJECT_ROOT;
24import static android.provider.DocumentsContract.METHOD_FIND_DOCUMENT_PATH;
25import static android.provider.DocumentsContract.METHOD_IS_CHILD_DOCUMENT;
26import static android.provider.DocumentsContract.METHOD_MOVE_DOCUMENT;
27import static android.provider.DocumentsContract.METHOD_REMOVE_DOCUMENT;
28import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT;
29import static android.provider.DocumentsContract.buildDocumentUri;
30import static android.provider.DocumentsContract.buildDocumentUriMaybeUsingTree;
31import static android.provider.DocumentsContract.buildTreeDocumentUri;
32import static android.provider.DocumentsContract.getDocumentId;
33import static android.provider.DocumentsContract.getRootId;
34import static android.provider.DocumentsContract.getSearchDocumentsQuery;
35import static android.provider.DocumentsContract.getTreeDocumentId;
36import static android.provider.DocumentsContract.isTreeUri;
37
38import android.Manifest;
39import android.annotation.CallSuper;
40import android.annotation.Nullable;
41import android.content.ClipDescription;
42import android.content.ContentProvider;
43import android.content.ContentResolver;
44import android.content.ContentValues;
45import android.content.Context;
46import android.content.Intent;
47import android.content.IntentSender;
48import android.content.UriMatcher;
49import android.content.pm.PackageManager;
50import android.content.pm.ProviderInfo;
51import android.content.res.AssetFileDescriptor;
52import android.database.Cursor;
53import android.graphics.Point;
54import android.net.Uri;
55import android.os.Bundle;
56import android.os.CancellationSignal;
57import android.os.OperationCanceledException;
58import android.os.ParcelFileDescriptor;
59import android.os.ParcelFileDescriptor.OnCloseListener;
60import android.provider.DocumentsContract.Document;
61import android.provider.DocumentsContract.Path;
62import android.provider.DocumentsContract.Root;
63import android.util.Log;
64
65import libcore.io.IoUtils;
66
67import java.io.FileNotFoundException;
68import java.util.LinkedList;
69import java.util.Objects;
70
71/**
72 * Base class for a document provider. A document provider offers read and write
73 * access to durable files, such as files stored on a local disk, or files in a
74 * cloud storage service. To create a document provider, extend this class,
75 * implement the abstract methods, and add it to your manifest like this:
76 *
77 * <pre class="prettyprint">&lt;manifest&gt;
78 *    ...
79 *    &lt;application&gt;
80 *        ...
81 *        &lt;provider
82 *            android:name="com.example.MyCloudProvider"
83 *            android:authorities="com.example.mycloudprovider"
84 *            android:exported="true"
85 *            android:grantUriPermissions="true"
86 *            android:permission="android.permission.MANAGE_DOCUMENTS"
87 *            android:enabled="@bool/isAtLeastKitKat"&gt;
88 *            &lt;intent-filter&gt;
89 *                &lt;action android:name="android.content.action.DOCUMENTS_PROVIDER" /&gt;
90 *            &lt;/intent-filter&gt;
91 *        &lt;/provider&gt;
92 *        ...
93 *    &lt;/application&gt;
94 *&lt;/manifest&gt;</pre>
95 * <p>
96 * When defining your provider, you must protect it with
97 * {@link android.Manifest.permission#MANAGE_DOCUMENTS}, which is a permission
98 * only the system can obtain. Applications cannot use a documents provider
99 * directly; they must go through {@link Intent#ACTION_OPEN_DOCUMENT} or
100 * {@link Intent#ACTION_CREATE_DOCUMENT} which requires a user to actively
101 * navigate and select documents. When a user selects documents through that UI,
102 * the system issues narrow URI permission grants to the requesting application.
103 * </p>
104 * <h3>Documents</h3>
105 * <p>
106 * A document can be either an openable stream (with a specific MIME type), or a
107 * directory containing additional documents (with the
108 * {@link Document#MIME_TYPE_DIR} MIME type). Each directory represents the top
109 * of a subtree containing zero or more documents, which can recursively contain
110 * even more documents and directories.
111 * </p>
112 * <p>
113 * Each document can have different capabilities, as described by
114 * {@link Document#COLUMN_FLAGS}. For example, if a document can be represented
115 * as a thumbnail, your provider can set
116 * {@link Document#FLAG_SUPPORTS_THUMBNAIL} and implement
117 * {@link #openDocumentThumbnail(String, Point, CancellationSignal)} to return
118 * that thumbnail.
119 * </p>
120 * <p>
121 * Each document under a provider is uniquely referenced by its
122 * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. A
123 * single document can be included in multiple directories when responding to
124 * {@link #queryChildDocuments(String, String[], String)}. For example, a
125 * provider might surface a single photo in multiple locations: once in a
126 * directory of geographic locations, and again in a directory of dates.
127 * </p>
128 * <h3>Roots</h3>
129 * <p>
130 * All documents are surfaced through one or more "roots." Each root represents
131 * the top of a document tree that a user can navigate. For example, a root
132 * could represent an account or a physical storage device. Similar to
133 * documents, each root can have capabilities expressed through
134 * {@link Root#COLUMN_FLAGS}.
135 * </p>
136 *
137 * @see Intent#ACTION_OPEN_DOCUMENT
138 * @see Intent#ACTION_OPEN_DOCUMENT_TREE
139 * @see Intent#ACTION_CREATE_DOCUMENT
140 */
141public abstract class DocumentsProvider extends ContentProvider {
142    private static final String TAG = "DocumentsProvider";
143
144    private static final int MATCH_ROOTS = 1;
145    private static final int MATCH_ROOT = 2;
146    private static final int MATCH_RECENT = 3;
147    private static final int MATCH_SEARCH = 4;
148    private static final int MATCH_DOCUMENT = 5;
149    private static final int MATCH_CHILDREN = 6;
150    private static final int MATCH_DOCUMENT_TREE = 7;
151    private static final int MATCH_CHILDREN_TREE = 8;
152
153    private String mAuthority;
154
155    private UriMatcher mMatcher;
156
157    /**
158     * Implementation is provided by the parent class.
159     */
160    @Override
161    public void attachInfo(Context context, ProviderInfo info) {
162        registerAuthority(info.authority);
163
164        // Sanity check our setup
165        if (!info.exported) {
166            throw new SecurityException("Provider must be exported");
167        }
168        if (!info.grantUriPermissions) {
169            throw new SecurityException("Provider must grantUriPermissions");
170        }
171        if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
172                || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
173            throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
174        }
175
176        super.attachInfo(context, info);
177    }
178
179    /** {@hide} */
180    @Override
181    public void attachInfoForTesting(Context context, ProviderInfo info) {
182        registerAuthority(info.authority);
183
184        super.attachInfoForTesting(context, info);
185    }
186
187    private void registerAuthority(String authority) {
188        mAuthority = authority;
189
190        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
191        mMatcher.addURI(mAuthority, "root", MATCH_ROOTS);
192        mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT);
193        mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT);
194        mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
195        mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
196        mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
197        mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE);
198        mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE);
199    }
200
201    /**
202     * Test if a document is descendant (child, grandchild, etc) from the given
203     * parent. For example, providers must implement this to support
204     * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. You should avoid making network
205     * requests to keep this request fast.
206     *
207     * @param parentDocumentId parent to verify against.
208     * @param documentId child to verify.
209     * @return if given document is a descendant of the given parent.
210     * @see DocumentsContract.Root#FLAG_SUPPORTS_IS_CHILD
211     */
212    public boolean isChildDocument(String parentDocumentId, String documentId) {
213        return false;
214    }
215
216    /** {@hide} */
217    private void enforceTree(Uri documentUri) {
218        if (isTreeUri(documentUri)) {
219            final String parent = getTreeDocumentId(documentUri);
220            final String child = getDocumentId(documentUri);
221            if (Objects.equals(parent, child)) {
222                return;
223            }
224            if (!isChildDocument(parent, child)) {
225                throw new SecurityException(
226                        "Document " + child + " is not a descendant of " + parent);
227            }
228        }
229    }
230
231    /**
232     * Create a new document and return its newly generated
233     * {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new
234     * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must
235     * not change once returned.
236     *
237     * @param parentDocumentId the parent directory to create the new document
238     *            under.
239     * @param mimeType the concrete MIME type associated with the new document.
240     *            If the MIME type is not supported, the provider must throw.
241     * @param displayName the display name of the new document. The provider may
242     *            alter this name to meet any internal constraints, such as
243     *            avoiding conflicting names.
244     */
245    @SuppressWarnings("unused")
246    public String createDocument(String parentDocumentId, String mimeType, String displayName)
247            throws FileNotFoundException {
248        throw new UnsupportedOperationException("Create not supported");
249    }
250
251    /**
252     * Rename an existing document.
253     * <p>
254     * If a different {@link Document#COLUMN_DOCUMENT_ID} must be used to
255     * represent the renamed document, generate and return it. Any outstanding
256     * URI permission grants will be updated to point at the new document. If
257     * the original {@link Document#COLUMN_DOCUMENT_ID} is still valid after the
258     * rename, return {@code null}.
259     *
260     * @param documentId the document to rename.
261     * @param displayName the updated display name of the document. The provider
262     *            may alter this name to meet any internal constraints, such as
263     *            avoiding conflicting names.
264     */
265    @SuppressWarnings("unused")
266    public String renameDocument(String documentId, String displayName)
267            throws FileNotFoundException {
268        throw new UnsupportedOperationException("Rename not supported");
269    }
270
271    /**
272     * Delete the requested document.
273     * <p>
274     * Upon returning, any URI permission grants for the given document will be
275     * revoked. If additional documents were deleted as a side effect of this
276     * call (such as documents inside a directory) the implementor is
277     * responsible for revoking those permissions using
278     * {@link #revokeDocumentPermission(String)}.
279     *
280     * @param documentId the document to delete.
281     */
282    @SuppressWarnings("unused")
283    public void deleteDocument(String documentId) throws FileNotFoundException {
284        throw new UnsupportedOperationException("Delete not supported");
285    }
286
287    /**
288     * Copy the requested document or a document tree.
289     * <p>
290     * Copies a document including all child documents to another location within
291     * the same document provider. Upon completion returns the document id of
292     * the copied document at the target destination. {@code null} must never
293     * be returned.
294     *
295     * @param sourceDocumentId the document to copy.
296     * @param targetParentDocumentId the target document to be copied into as a child.
297     */
298    @SuppressWarnings("unused")
299    public String copyDocument(String sourceDocumentId, String targetParentDocumentId)
300            throws FileNotFoundException {
301        throw new UnsupportedOperationException("Copy not supported");
302    }
303
304    /**
305     * Move the requested document or a document tree.
306     *
307     * <p>Moves a document including all child documents to another location within
308     * the same document provider. Upon completion returns the document id of
309     * the copied document at the target destination. {@code null} must never
310     * be returned.
311     *
312     * <p>It's the responsibility of the provider to revoke grants if the document
313     * is no longer accessible using <code>sourceDocumentId</code>.
314     *
315     * @param sourceDocumentId the document to move.
316     * @param sourceParentDocumentId the parent of the document to move.
317     * @param targetParentDocumentId the target document to be a new parent of the
318     *     source document.
319     */
320    @SuppressWarnings("unused")
321    public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
322            String targetParentDocumentId)
323            throws FileNotFoundException {
324        throw new UnsupportedOperationException("Move not supported");
325    }
326
327    /**
328     * Removes the requested document or a document tree.
329     *
330     * <p>In contrast to {@link #deleteDocument} it requires specifying the parent.
331     * This method is especially useful if the document can be in multiple parents.
332     *
333     * <p>It's the responsibility of the provider to revoke grants if the document is
334     * removed from the last parent, and effectively the document is deleted.
335     *
336     * @param documentId the document to remove.
337     * @param parentDocumentId the parent of the document to move.
338     */
339    @SuppressWarnings("unused")
340    public void removeDocument(String documentId, String parentDocumentId)
341            throws FileNotFoundException {
342        throw new UnsupportedOperationException("Remove not supported");
343    }
344
345    /**
346     * Finds the canonical path for the requested document. The path must start
347     * from the parent document if parentDocumentId is not null or the root document
348     * if parentDocumentId is null. If there are more than one path to this document,
349     * return the most typical one. Include both the parent document or root document
350     * and the requested document in the returned path.
351     *
352     * <p>This API assumes that document ID has enough info to infer the root.
353     * Different roots should use different document ID to refer to the same
354     * document.
355     *
356     * @param parentDocumentId the document from which the path starts if not null,
357     *     or null to indicate a path from the root is requested.
358     * @param childDocumentId the document which path is requested.
359     * @return the path of the requested document. If parentDocumentId is null
360     *     returned root ID must not be null. If parentDocumentId is not null
361     *     returned root ID must be null.
362     */
363    public Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId)
364            throws FileNotFoundException {
365        throw new UnsupportedOperationException("findDocumentPath not supported.");
366    }
367
368    /**
369     * Creates an intent sender for a web link, if the document is web linkable.
370     * <p>
371     * Before any new permissions are granted for the linked document, a visible
372     * UI must be shown, so the user can explicitly confirm whether the permission
373     * grants are expected. The user must be able to cancel the operation.
374     * <p>
375     * Options passed as an argument may include a list of recipients, such
376     * as email addresses. The provider should reflect these options if possible,
377     * but it's acceptable to ignore them. In either case, confirmation UI must
378     * be shown before any new permission grants are granted.
379     * <p>
380     * It is all right to generate a web link without granting new permissions,
381     * if opening the link would result in a page for requesting permission
382     * access. If it's impossible then the operation must fail by throwing an exception.
383     *
384     * @param documentId the document to create a web link intent for.
385     * @param options additional information, such as list of recipients. Optional.
386     *
387     * @see DocumentsContract.Document#FLAG_WEB_LINKABLE
388     * @see android.app.PendingIntent#getIntentSender
389     */
390    public IntentSender createWebLinkIntent(String documentId, @Nullable Bundle options)
391            throws FileNotFoundException {
392        throw new UnsupportedOperationException("createWebLink is not supported.");
393    }
394
395    /**
396     * Return all roots currently provided. To display to users, you must define
397     * at least one root. You should avoid making network requests to keep this
398     * request fast.
399     * <p>
400     * Each root is defined by the metadata columns described in {@link Root},
401     * including {@link Root#COLUMN_DOCUMENT_ID} which points to a directory
402     * representing a tree of documents to display under that root.
403     * <p>
404     * If this set of roots changes, you must call {@link ContentResolver#notifyChange(Uri,
405     * android.database.ContentObserver, boolean)} with
406     * {@link DocumentsContract#buildRootsUri(String)} to notify the system.
407     *
408     * @param projection list of {@link Root} columns to put into the cursor. If
409     *            {@code null} all supported columns should be included.
410     */
411    public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException;
412
413    /**
414     * Return recently modified documents under the requested root. This will
415     * only be called for roots that advertise
416     * {@link Root#FLAG_SUPPORTS_RECENTS}. The returned documents should be
417     * sorted by {@link Document#COLUMN_LAST_MODIFIED} in descending order, and
418     * limited to only return the 64 most recently modified documents.
419     * <p>
420     * Recent documents do not support change notifications.
421     *
422     * @param projection list of {@link Document} columns to put into the
423     *            cursor. If {@code null} all supported columns should be
424     *            included.
425     * @see DocumentsContract#EXTRA_LOADING
426     */
427    @SuppressWarnings("unused")
428    public Cursor queryRecentDocuments(String rootId, String[] projection)
429            throws FileNotFoundException {
430        throw new UnsupportedOperationException("Recent not supported");
431    }
432
433    /**
434     * Return metadata for the single requested document. You should avoid
435     * making network requests to keep this request fast.
436     *
437     * @param documentId the document to return.
438     * @param projection list of {@link Document} columns to put into the
439     *            cursor. If {@code null} all supported columns should be
440     *            included.
441     */
442    public abstract Cursor queryDocument(String documentId, String[] projection)
443            throws FileNotFoundException;
444
445    /**
446     * Return the children documents contained in the requested directory. This
447     * must only return immediate descendants, as additional queries will be
448     * issued to recursively explore the tree.
449     * <p>
450     * Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher
451     * should override {@link #queryChildDocuments(String, String[], Bundle)}.
452     * <p>
453     * If your provider is cloud-based, and you have some data cached or pinned
454     * locally, you may return the local data immediately, setting
455     * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that
456     * you are still fetching additional data. Then, when the network data is
457     * available, you can send a change notification to trigger a requery and
458     * return the complete contents. To return a Cursor with extras, you need to
459     * extend and override {@link Cursor#getExtras()}.
460     * <p>
461     * To support change notifications, you must
462     * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant
463     * Uri, such as
464     * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then
465     * you can call {@link ContentResolver#notifyChange(Uri,
466     * android.database.ContentObserver, boolean)} with that Uri to send change
467     * notifications.
468     *
469     * @param parentDocumentId the directory to return children for.
470     * @param projection list of {@link Document} columns to put into the
471     *            cursor. If {@code null} all supported columns should be
472     *            included.
473     * @param sortOrder how to order the rows, formatted as an SQL
474     *            {@code ORDER BY} clause (excluding the ORDER BY itself).
475     *            Passing {@code null} will use the default sort order, which
476     *            may be unordered. This ordering is a hint that can be used to
477     *            prioritize how data is fetched from the network, but UI may
478     *            always enforce a specific ordering.
479     * @see DocumentsContract#EXTRA_LOADING
480     * @see DocumentsContract#EXTRA_INFO
481     * @see DocumentsContract#EXTRA_ERROR
482     */
483    public abstract Cursor queryChildDocuments(
484            String parentDocumentId, String[] projection, String sortOrder)
485            throws FileNotFoundException;
486
487    /**
488     * Override this method to return the children documents contained
489     * in the requested directory. This must return immediate descendants only.
490     *
491     * <p>If your provider is cloud-based, and you have data cached
492     * locally, you may return the local data immediately, setting
493     * {@link DocumentsContract#EXTRA_LOADING} on Cursor extras to indicate that
494     * you are still fetching additional data. Then, when the network data is
495     * available, you can send a change notification to trigger a requery and
496     * return the complete contents. To return a Cursor with extras, you need to
497     * extend and override {@link Cursor#getExtras()}.
498     *
499     * <p>To support change notifications, you must
500     * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant
501     * Uri, such as
502     * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then
503     * you can call {@link ContentResolver#notifyChange(Uri,
504     * android.database.ContentObserver, boolean)} with that Uri to send change
505     * notifications.
506     *
507     * @param parentDocumentId the directory to return children for.
508     * @param projection list of {@link Document} columns to put into the
509     *            cursor. If {@code null} all supported columns should be
510     *            included.
511     * @param queryArgs Bundle containing sorting information or other
512     *            argument useful to the provider. If no sorting
513     *            information is available, default sorting
514     *            will be used, which may be unordered. See
515     *            {@link ContentResolver#QUERY_ARG_SORT_COLUMNS} for
516     *            details.
517     *
518     * @see DocumentsContract#EXTRA_LOADING
519     * @see DocumentsContract#EXTRA_INFO
520     * @see DocumentsContract#EXTRA_ERROR
521     */
522    public Cursor queryChildDocuments(
523            String parentDocumentId, @Nullable String[] projection, @Nullable Bundle queryArgs)
524            throws FileNotFoundException {
525
526        return queryChildDocuments(
527                parentDocumentId, projection, getSortClause(queryArgs));
528    }
529
530    /** {@hide} */
531    @SuppressWarnings("unused")
532    public Cursor queryChildDocumentsForManage(
533            String parentDocumentId, @Nullable String[] projection, @Nullable String sortOrder)
534            throws FileNotFoundException {
535        throw new UnsupportedOperationException("Manage not supported");
536    }
537
538    /**
539     * Return documents that match the given query under the requested
540     * root. The returned documents should be sorted by relevance in descending
541     * order. How documents are matched against the query string is an
542     * implementation detail left to each provider, but it's suggested that at
543     * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a
544     * case-insensitive fashion.
545     * <p>
546     * If your provider is cloud-based, and you have some data cached or pinned
547     * locally, you may return the local data immediately, setting
548     * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that
549     * you are still fetching additional data. Then, when the network data is
550     * available, you can send a change notification to trigger a requery and
551     * return the complete contents.
552     * <p>
553     * To support change notifications, you must
554     * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant
555     * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String,
556     * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri,
557     * android.database.ContentObserver, boolean)} with that Uri to send change
558     * notifications.
559     *
560     * @param rootId the root to search under.
561     * @param query string to match documents against.
562     * @param projection list of {@link Document} columns to put into the
563     *            cursor. If {@code null} all supported columns should be
564     *            included.
565     * @see DocumentsContract#EXTRA_LOADING
566     * @see DocumentsContract#EXTRA_INFO
567     * @see DocumentsContract#EXTRA_ERROR
568     */
569    @SuppressWarnings("unused")
570    public Cursor querySearchDocuments(String rootId, String query, String[] projection)
571            throws FileNotFoundException {
572        throw new UnsupportedOperationException("Search not supported");
573    }
574
575    /** {@hide} */
576    @SuppressWarnings("unused")
577    public boolean ejectRoot(String rootId) {
578        throw new UnsupportedOperationException("Eject not supported");
579    }
580
581    /**
582     * Return concrete MIME type of the requested document. Must match the value
583     * of {@link Document#COLUMN_MIME_TYPE} for this document. The default
584     * implementation queries {@link #queryDocument(String, String[])}, so
585     * providers may choose to override this as an optimization.
586     */
587    public String getDocumentType(String documentId) throws FileNotFoundException {
588        final Cursor cursor = queryDocument(documentId, null);
589        try {
590            if (cursor.moveToFirst()) {
591                return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
592            } else {
593                return null;
594            }
595        } finally {
596            IoUtils.closeQuietly(cursor);
597        }
598    }
599
600    /**
601     * Open and return the requested document.
602     * <p>
603     * Your provider should return a reliable {@link ParcelFileDescriptor} to
604     * detect when the remote caller has finished reading or writing the
605     * document. You may return a pipe or socket pair if the mode is exclusively
606     * "r" or "w", but complex modes like "rw" imply a normal file on disk that
607     * supports seeking.
608     * <p>
609     * If you block while downloading content, you should periodically check
610     * {@link CancellationSignal#isCanceled()} to abort abandoned open requests.
611     *
612     * @param documentId the document to return.
613     * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
614     * @param signal used by the caller to signal if the request should be
615     *            cancelled. May be null.
616     * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
617     *      OnCloseListener)
618     * @see ParcelFileDescriptor#createReliablePipe()
619     * @see ParcelFileDescriptor#createReliableSocketPair()
620     * @see ParcelFileDescriptor#parseMode(String)
621     */
622    public abstract ParcelFileDescriptor openDocument(
623            String documentId, String mode, CancellationSignal signal) throws FileNotFoundException;
624
625    /**
626     * Open and return a thumbnail of the requested document.
627     * <p>
628     * A provider should return a thumbnail closely matching the hinted size,
629     * attempting to serve from a local cache if possible. A provider should
630     * never return images more than double the hinted size.
631     * <p>
632     * If you perform expensive operations to download or generate a thumbnail,
633     * you should periodically check {@link CancellationSignal#isCanceled()} to
634     * abort abandoned thumbnail requests.
635     *
636     * @param documentId the document to return.
637     * @param sizeHint hint of the optimal thumbnail dimensions.
638     * @param signal used by the caller to signal if the request should be
639     *            cancelled. May be null.
640     * @see Document#FLAG_SUPPORTS_THUMBNAIL
641     */
642    @SuppressWarnings("unused")
643    public AssetFileDescriptor openDocumentThumbnail(
644            String documentId, Point sizeHint, CancellationSignal signal)
645            throws FileNotFoundException {
646        throw new UnsupportedOperationException("Thumbnails not supported");
647    }
648
649    /**
650     * Open and return the document in a format matching the specified MIME
651     * type filter.
652     * <p>
653     * A provider may perform a conversion if the documents's MIME type is not
654     * matching the specified MIME type filter.
655     * <p>
656     * Virtual documents must have at least one streamable format.
657     *
658     * @param documentId the document to return.
659     * @param mimeTypeFilter the MIME type filter for the requested format. May
660     *            be *\/*, which matches any MIME type.
661     * @param opts extra options from the client. Specific to the content
662     *            provider.
663     * @param signal used by the caller to signal if the request should be
664     *            cancelled. May be null.
665     * @see #getDocumentStreamTypes(String, String)
666     */
667    @SuppressWarnings("unused")
668    public AssetFileDescriptor openTypedDocument(
669            String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
670            throws FileNotFoundException {
671        throw new FileNotFoundException("The requested MIME type is not supported.");
672    }
673
674    @Override
675    public final Cursor query(Uri uri, String[] projection, String selection,
676            String[] selectionArgs, String sortOrder) {
677        // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary
678        // transport method. We override that, and don't ever delegate to this method.
679        throw new UnsupportedOperationException("Pre-Android-O query format not supported.");
680    }
681
682    /**
683     * WARNING: Sub-classes should not override this method. This method is non-final
684     * solely for the purposes of backwards compatibility.
685     *
686     * @see #queryChildDocuments(String, String[], Bundle),
687     *      {@link #queryDocument(String, String[])},
688     *      {@link #queryRecentDocuments(String, String[])},
689     *      {@link #queryRoots(String[])}, and
690     *      {@link #querySearchDocuments(String, String, String[])}.
691     */
692    @Override
693    public Cursor query(Uri uri, String[] projection, String selection,
694            String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
695        // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary
696        // transport method. We override that, and don't ever delegate to this metohd.
697        throw new UnsupportedOperationException("Pre-Android-O query format not supported.");
698    }
699
700    /**
701     * Implementation is provided by the parent class. Cannot be overriden.
702     *
703     * @see #queryRoots(String[])
704     * @see #queryRecentDocuments(String, String[])
705     * @see #queryDocument(String, String[])
706     * @see #queryChildDocuments(String, String[], String)
707     * @see #querySearchDocuments(String, String, String[])
708     */
709    @Override
710    public final Cursor query(
711            Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) {
712        try {
713            switch (mMatcher.match(uri)) {
714                case MATCH_ROOTS:
715                    return queryRoots(projection);
716                case MATCH_RECENT:
717                    return queryRecentDocuments(getRootId(uri), projection);
718                case MATCH_SEARCH:
719                    return querySearchDocuments(
720                            getRootId(uri), getSearchDocumentsQuery(uri), projection);
721                case MATCH_DOCUMENT:
722                case MATCH_DOCUMENT_TREE:
723                    enforceTree(uri);
724                    return queryDocument(getDocumentId(uri), projection);
725                case MATCH_CHILDREN:
726                case MATCH_CHILDREN_TREE:
727                    enforceTree(uri);
728                    if (DocumentsContract.isManageMode(uri)) {
729                        // TODO: Update "ForManage" variant to support query args.
730                        return queryChildDocumentsForManage(
731                                getDocumentId(uri),
732                                projection,
733                                getSortClause(queryArgs));
734                    } else {
735                        return queryChildDocuments(getDocumentId(uri), projection, queryArgs);
736                    }
737                default:
738                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
739            }
740        } catch (FileNotFoundException e) {
741            Log.w(TAG, "Failed during query", e);
742            return null;
743        }
744    }
745
746    private static @Nullable String getSortClause(@Nullable Bundle queryArgs) {
747        queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY;
748        String sortClause = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER);
749
750        if (sortClause == null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) {
751            sortClause = ContentResolver.createSqlSortClause(queryArgs);
752        }
753
754        return sortClause;
755    }
756
757    /**
758     * Implementation is provided by the parent class. Cannot be overriden.
759     *
760     * @see #getDocumentType(String)
761     */
762    @Override
763    public final String getType(Uri uri) {
764        try {
765            switch (mMatcher.match(uri)) {
766                case MATCH_ROOT:
767                    return DocumentsContract.Root.MIME_TYPE_ITEM;
768                case MATCH_DOCUMENT:
769                case MATCH_DOCUMENT_TREE:
770                    enforceTree(uri);
771                    return getDocumentType(getDocumentId(uri));
772                default:
773                    return null;
774            }
775        } catch (FileNotFoundException e) {
776            Log.w(TAG, "Failed during getType", e);
777            return null;
778        }
779    }
780
781    /**
782     * Implementation is provided by the parent class. Can be overridden to
783     * provide additional functionality, but subclasses <em>must</em> always
784     * call the superclass. If the superclass returns {@code null}, the subclass
785     * may implement custom behavior.
786     * <p>
787     * This is typically used to resolve a subtree URI into a concrete document
788     * reference, issuing a narrower single-document URI permission grant along
789     * the way.
790     *
791     * @see DocumentsContract#buildDocumentUriUsingTree(Uri, String)
792     */
793    @CallSuper
794    @Override
795    public Uri canonicalize(Uri uri) {
796        final Context context = getContext();
797        switch (mMatcher.match(uri)) {
798            case MATCH_DOCUMENT_TREE:
799                enforceTree(uri);
800
801                final Uri narrowUri = buildDocumentUri(uri.getAuthority(), getDocumentId(uri));
802
803                // Caller may only have prefix grant, so extend them a grant to
804                // the narrow URI.
805                final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, uri);
806                context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags);
807                return narrowUri;
808        }
809        return null;
810    }
811
812    private static int getCallingOrSelfUriPermissionModeFlags(Context context, Uri uri) {
813        // TODO: move this to a direct AMS call
814        int modeFlags = 0;
815        if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
816                == PackageManager.PERMISSION_GRANTED) {
817            modeFlags |= Intent.FLAG_GRANT_READ_URI_PERMISSION;
818        }
819        if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
820                == PackageManager.PERMISSION_GRANTED) {
821            modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
822        }
823        if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION
824                | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
825                == PackageManager.PERMISSION_GRANTED) {
826            modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
827        }
828        return modeFlags;
829    }
830
831    /**
832     * Implementation is provided by the parent class. Throws by default, and
833     * cannot be overriden.
834     *
835     * @see #createDocument(String, String, String)
836     */
837    @Override
838    public final Uri insert(Uri uri, ContentValues values) {
839        throw new UnsupportedOperationException("Insert not supported");
840    }
841
842    /**
843     * Implementation is provided by the parent class. Throws by default, and
844     * cannot be overriden.
845     *
846     * @see #deleteDocument(String)
847     */
848    @Override
849    public final int delete(Uri uri, String selection, String[] selectionArgs) {
850        throw new UnsupportedOperationException("Delete not supported");
851    }
852
853    /**
854     * Implementation is provided by the parent class. Throws by default, and
855     * cannot be overriden.
856     */
857    @Override
858    public final int update(
859            Uri uri, ContentValues values, String selection, String[] selectionArgs) {
860        throw new UnsupportedOperationException("Update not supported");
861    }
862
863    /**
864     * Implementation is provided by the parent class. Can be overridden to
865     * provide additional functionality, but subclasses <em>must</em> always
866     * call the superclass. If the superclass returns {@code null}, the subclass
867     * may implement custom behavior.
868     */
869    @CallSuper
870    @Override
871    public Bundle call(String method, String arg, Bundle extras) {
872        if (!method.startsWith("android:")) {
873            // Ignore non-platform methods
874            return super.call(method, arg, extras);
875        }
876
877        try {
878            return callUnchecked(method, arg, extras);
879        } catch (FileNotFoundException e) {
880            throw new IllegalStateException("Failed call " + method, e);
881        }
882    }
883
884    private Bundle callUnchecked(String method, String arg, Bundle extras)
885            throws FileNotFoundException {
886
887        final Context context = getContext();
888        final Bundle out = new Bundle();
889
890        if (METHOD_EJECT_ROOT.equals(method)) {
891            // Given that certain system apps can hold MOUNT_UNMOUNT permission, but only apps
892            // signed with platform signature can hold MANAGE_DOCUMENTS, we are going to check for
893            // MANAGE_DOCUMENTS here instead
894            getContext().enforceCallingPermission(
895                    android.Manifest.permission.MANAGE_DOCUMENTS, null);
896            final Uri rootUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
897            final String rootId = DocumentsContract.getRootId(rootUri);
898            final boolean ejected = ejectRoot(rootId);
899
900            out.putBoolean(DocumentsContract.EXTRA_RESULT, ejected);
901
902            return out;
903        }
904
905        final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
906        final String authority = documentUri.getAuthority();
907        final String documentId = DocumentsContract.getDocumentId(documentUri);
908
909        if (!mAuthority.equals(authority)) {
910            throw new SecurityException(
911                    "Requested authority " + authority + " doesn't match provider " + mAuthority);
912        }
913
914        // If the URI is a tree URI performs some validation.
915        enforceTree(documentUri);
916
917        if (METHOD_IS_CHILD_DOCUMENT.equals(method)) {
918            enforceReadPermissionInner(documentUri, getCallingPackage(), null);
919
920            final Uri childUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI);
921            final String childAuthority = childUri.getAuthority();
922            final String childId = DocumentsContract.getDocumentId(childUri);
923
924            out.putBoolean(
925                    DocumentsContract.EXTRA_RESULT,
926                    mAuthority.equals(childAuthority)
927                            && isChildDocument(documentId, childId));
928
929        } else if (METHOD_CREATE_DOCUMENT.equals(method)) {
930            enforceWritePermissionInner(documentUri, getCallingPackage(), null);
931
932            final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
933            final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
934            final String newDocumentId = createDocument(documentId, mimeType, displayName);
935
936            // No need to issue new grants here, since caller either has
937            // manage permission or a prefix grant. We might generate a
938            // tree style URI if that's how they called us.
939            final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri,
940                    newDocumentId);
941            out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
942
943        } else if (METHOD_CREATE_WEB_LINK_INTENT.equals(method)) {
944            enforceWritePermissionInner(documentUri, getCallingPackage(), null);
945
946            final Bundle options = extras.getBundle(DocumentsContract.EXTRA_OPTIONS);
947            final IntentSender intentSender = createWebLinkIntent(documentId, options);
948
949            out.putParcelable(DocumentsContract.EXTRA_RESULT, intentSender);
950
951        } else if (METHOD_RENAME_DOCUMENT.equals(method)) {
952            enforceWritePermissionInner(documentUri, getCallingPackage(), null);
953
954            final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
955            final String newDocumentId = renameDocument(documentId, displayName);
956
957            if (newDocumentId != null) {
958                final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri,
959                        newDocumentId);
960
961                // If caller came in with a narrow grant, issue them a
962                // narrow grant for the newly renamed document.
963                if (!isTreeUri(newDocumentUri)) {
964                    final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context,
965                            documentUri);
966                    context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags);
967                }
968
969                out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
970
971                // Original document no longer exists, clean up any grants.
972                revokeDocumentPermission(documentId);
973            }
974
975        } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
976            enforceWritePermissionInner(documentUri, getCallingPackage(), null);
977            deleteDocument(documentId);
978
979            // Document no longer exists, clean up any grants.
980            revokeDocumentPermission(documentId);
981
982        } else if (METHOD_COPY_DOCUMENT.equals(method)) {
983            final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI);
984            final String targetId = DocumentsContract.getDocumentId(targetUri);
985
986            enforceReadPermissionInner(documentUri, getCallingPackage(), null);
987            enforceWritePermissionInner(targetUri, getCallingPackage(), null);
988
989            final String newDocumentId = copyDocument(documentId, targetId);
990
991            if (newDocumentId != null) {
992                final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri,
993                        newDocumentId);
994
995                if (!isTreeUri(newDocumentUri)) {
996                    final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context,
997                            documentUri);
998                    context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags);
999                }
1000
1001                out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
1002            }
1003
1004        } else if (METHOD_MOVE_DOCUMENT.equals(method)) {
1005            final Uri parentSourceUri = extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI);
1006            final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri);
1007            final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI);
1008            final String targetId = DocumentsContract.getDocumentId(targetUri);
1009
1010            enforceWritePermissionInner(documentUri, getCallingPackage(), null);
1011            enforceReadPermissionInner(parentSourceUri, getCallingPackage(), null);
1012            enforceWritePermissionInner(targetUri, getCallingPackage(), null);
1013
1014            final String newDocumentId = moveDocument(documentId, parentSourceId, targetId);
1015
1016            if (newDocumentId != null) {
1017                final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri,
1018                        newDocumentId);
1019
1020                if (!isTreeUri(newDocumentUri)) {
1021                    final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context,
1022                            documentUri);
1023                    context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags);
1024                }
1025
1026                out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
1027            }
1028
1029        } else if (METHOD_REMOVE_DOCUMENT.equals(method)) {
1030            final Uri parentSourceUri = extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI);
1031            final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri);
1032
1033            enforceReadPermissionInner(parentSourceUri, getCallingPackage(), null);
1034            enforceWritePermissionInner(documentUri, getCallingPackage(), null);
1035            removeDocument(documentId, parentSourceId);
1036
1037            // It's responsibility of the provider to revoke any grants, as the document may be
1038            // still attached to another parents.
1039        } else if (METHOD_FIND_DOCUMENT_PATH.equals(method)) {
1040            final boolean isTreeUri = isTreeUri(documentUri);
1041
1042            if (isTreeUri) {
1043                enforceReadPermissionInner(documentUri, getCallingPackage(), null);
1044            } else {
1045                getContext().enforceCallingPermission(Manifest.permission.MANAGE_DOCUMENTS, null);
1046            }
1047
1048            final String parentDocumentId = isTreeUri
1049                    ? DocumentsContract.getTreeDocumentId(documentUri)
1050                    : null;
1051
1052            Path path = findDocumentPath(parentDocumentId, documentId);
1053
1054            // Ensure provider doesn't leak information to unprivileged callers.
1055            if (isTreeUri) {
1056                if (!Objects.equals(path.getPath().get(0), parentDocumentId)) {
1057                    Log.wtf(TAG, "Provider doesn't return path from the tree root. Expected: "
1058                            + parentDocumentId + " found: " + path.getPath().get(0));
1059
1060                    LinkedList<String> docs = new LinkedList<>(path.getPath());
1061                    while (docs.size() > 1 && !Objects.equals(docs.getFirst(), parentDocumentId)) {
1062                        docs.removeFirst();
1063                    }
1064                    path = new Path(null, docs);
1065                }
1066
1067                if (path.getRootId() != null) {
1068                    Log.wtf(TAG, "Provider returns root id :"
1069                            + path.getRootId() + " unexpectedly. Erase root id.");
1070                    path = new Path(null, path.getPath());
1071                }
1072            }
1073
1074            out.putParcelable(DocumentsContract.EXTRA_RESULT, path);
1075        } else {
1076            throw new UnsupportedOperationException("Method not supported " + method);
1077        }
1078
1079        return out;
1080    }
1081
1082    /**
1083     * Revoke any active permission grants for the given
1084     * {@link Document#COLUMN_DOCUMENT_ID}, usually called when a document
1085     * becomes invalid. Follows the same semantics as
1086     * {@link Context#revokeUriPermission(Uri, int)}.
1087     */
1088    public final void revokeDocumentPermission(String documentId) {
1089        final Context context = getContext();
1090        context.revokeUriPermission(buildDocumentUri(mAuthority, documentId), ~0);
1091        context.revokeUriPermission(buildTreeDocumentUri(mAuthority, documentId), ~0);
1092    }
1093
1094    /**
1095     * Implementation is provided by the parent class. Cannot be overriden.
1096     *
1097     * @see #openDocument(String, String, CancellationSignal)
1098     */
1099    @Override
1100    public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
1101        enforceTree(uri);
1102        return openDocument(getDocumentId(uri), mode, null);
1103    }
1104
1105    /**
1106     * Implementation is provided by the parent class. Cannot be overriden.
1107     *
1108     * @see #openDocument(String, String, CancellationSignal)
1109     */
1110    @Override
1111    public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
1112            throws FileNotFoundException {
1113        enforceTree(uri);
1114        return openDocument(getDocumentId(uri), mode, signal);
1115    }
1116
1117    /**
1118     * Implementation is provided by the parent class. Cannot be overriden.
1119     *
1120     * @see #openDocument(String, String, CancellationSignal)
1121     */
1122    @Override
1123    @SuppressWarnings("resource")
1124    public final AssetFileDescriptor openAssetFile(Uri uri, String mode)
1125            throws FileNotFoundException {
1126        enforceTree(uri);
1127        final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null);
1128        return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
1129    }
1130
1131    /**
1132     * Implementation is provided by the parent class. Cannot be overriden.
1133     *
1134     * @see #openDocument(String, String, CancellationSignal)
1135     */
1136    @Override
1137    @SuppressWarnings("resource")
1138    public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal)
1139            throws FileNotFoundException {
1140        enforceTree(uri);
1141        final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal);
1142        return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
1143    }
1144
1145    /**
1146     * Implementation is provided by the parent class. Cannot be overriden.
1147     *
1148     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
1149     * @see #openTypedDocument(String, String, Bundle, CancellationSignal)
1150     * @see #getDocumentStreamTypes(String, String)
1151     */
1152    @Override
1153    public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
1154            throws FileNotFoundException {
1155        return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, null);
1156    }
1157
1158    /**
1159     * Implementation is provided by the parent class. Cannot be overriden.
1160     *
1161     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
1162     * @see #openTypedDocument(String, String, Bundle, CancellationSignal)
1163     * @see #getDocumentStreamTypes(String, String)
1164     */
1165    @Override
1166    public final AssetFileDescriptor openTypedAssetFile(
1167            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
1168            throws FileNotFoundException {
1169        return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, signal);
1170    }
1171
1172    /**
1173     * Return a list of streamable MIME types matching the filter, which can be passed to
1174     * {@link #openTypedDocument(String, String, Bundle, CancellationSignal)}.
1175     *
1176     * <p>The default implementation returns a MIME type provided by
1177     * {@link #queryDocument(String, String[])} as long as it matches the filter and the document
1178     * does not have the {@link Document#FLAG_VIRTUAL_DOCUMENT} flag set.
1179     *
1180     * <p>Virtual documents must have at least one streamable format.
1181     *
1182     * @see #getStreamTypes(Uri, String)
1183     * @see #openTypedDocument(String, String, Bundle, CancellationSignal)
1184     */
1185    public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {
1186        Cursor cursor = null;
1187        try {
1188            cursor = queryDocument(documentId, null);
1189            if (cursor.moveToFirst()) {
1190                final String mimeType =
1191                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
1192                final long flags =
1193                    cursor.getLong(cursor.getColumnIndexOrThrow(Document.COLUMN_FLAGS));
1194                if ((flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0 && mimeType != null &&
1195                        mimeTypeMatches(mimeTypeFilter, mimeType)) {
1196                    return new String[] { mimeType };
1197                }
1198            }
1199        } catch (FileNotFoundException e) {
1200            return null;
1201        } finally {
1202            IoUtils.closeQuietly(cursor);
1203        }
1204
1205        // No streamable MIME types.
1206        return null;
1207    }
1208
1209    /**
1210     * Called by a client to determine the types of data streams that this content provider
1211     * support for the given URI.
1212     *
1213     * <p>Overriding this method is deprecated. Override {@link #openTypedDocument} instead.
1214     *
1215     * @see #getDocumentStreamTypes(String, String)
1216     */
1217    @Override
1218    public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
1219        enforceTree(uri);
1220        return getDocumentStreamTypes(getDocumentId(uri), mimeTypeFilter);
1221    }
1222
1223    /**
1224     * @hide
1225     */
1226    private final AssetFileDescriptor openTypedAssetFileImpl(
1227            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
1228            throws FileNotFoundException {
1229        enforceTree(uri);
1230        final String documentId = getDocumentId(uri);
1231        if (opts != null && opts.containsKey(ContentResolver.EXTRA_SIZE)) {
1232            final Point sizeHint = opts.getParcelable(ContentResolver.EXTRA_SIZE);
1233            return openDocumentThumbnail(documentId, sizeHint, signal);
1234        }
1235        if ("*/*".equals(mimeTypeFilter)) {
1236             // If they can take anything, the untyped open call is good enough.
1237             return openAssetFile(uri, "r");
1238        }
1239        final String baseType = getType(uri);
1240        if (baseType != null && ClipDescription.compareMimeTypes(baseType, mimeTypeFilter)) {
1241            // Use old untyped open call if this provider has a type for this
1242            // URI and it matches the request.
1243            return openAssetFile(uri, "r");
1244        }
1245        // For any other yet unhandled case, let the provider subclass handle it.
1246        return openTypedDocument(documentId, mimeTypeFilter, opts, signal);
1247    }
1248
1249    /**
1250     * @hide
1251     */
1252    public static boolean mimeTypeMatches(String filter, String test) {
1253        if (test == null) {
1254            return false;
1255        } else if (filter == null || "*/*".equals(filter)) {
1256            return true;
1257        } else if (filter.equals(test)) {
1258            return true;
1259        } else if (filter.endsWith("/*")) {
1260            return filter.regionMatches(0, test, 0, filter.indexOf('/'));
1261        } else {
1262            return false;
1263        }
1264    }
1265}
1266