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