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
80 * UI, the system issues narrow URI permission grants to the requesting
81 * 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, a provider can set {@link Document#FLAG_SUPPORTS_THUMBNAIL}
95 * 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 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 */
119public 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}. A provider 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. A provider must define at least one
198     * root to display to users, and it should avoid making network requests to
199     * keep this 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)} 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     *
220     * @param projection list of {@link Document} columns to put into the
221     *            cursor. If {@code null} all supported columns should be
222     *            included.
223     * @see DocumentsContract#EXTRA_LOADING
224     */
225    @SuppressWarnings("unused")
226    public Cursor queryRecentDocuments(String rootId, String[] projection)
227            throws FileNotFoundException {
228        throw new UnsupportedOperationException("Recent not supported");
229    }
230
231    /**
232     * Return metadata for the single requested document. A provider should
233     * avoid making network requests to keep this request fast.
234     *
235     * @param documentId the document to return.
236     * @param projection list of {@link Document} columns to put into the
237     *            cursor. If {@code null} all supported columns should be
238     *            included.
239     */
240    public abstract Cursor queryDocument(String documentId, String[] projection)
241            throws FileNotFoundException;
242
243    /**
244     * Return the children documents contained in the requested directory. This
245     * must only return immediate descendants, as additional queries will be
246     * issued to recursively explore the tree.
247     * <p>
248     * If your provider is cloud-based, and you have some data cached or pinned
249     * locally, you may return the local data immediately, setting
250     * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that
251     * your provider is still fetching additional data. Then, when the network
252     * data is available, you can call {@link ContentResolver#notifyChange(Uri,
253     * android.database.ContentObserver)} to trigger a requery and return the
254     * complete contents.
255     *
256     * @param parentDocumentId the directory to return children for.
257     * @param projection list of {@link Document} columns to put into the
258     *            cursor. If {@code null} all supported columns should be
259     *            included.
260     * @param sortOrder how to order the rows, formatted as an SQL
261     *            {@code ORDER BY} clause (excluding the ORDER BY itself).
262     *            Passing {@code null} will use the default sort order, which
263     *            may be unordered. This ordering is a hint that can be used to
264     *            prioritize how data is fetched from the network, but UI may
265     *            always enforce a specific ordering.
266     * @see DocumentsContract#EXTRA_LOADING
267     * @see DocumentsContract#EXTRA_INFO
268     * @see DocumentsContract#EXTRA_ERROR
269     */
270    public abstract Cursor queryChildDocuments(
271            String parentDocumentId, String[] projection, String sortOrder)
272            throws FileNotFoundException;
273
274    /** {@hide} */
275    @SuppressWarnings("unused")
276    public Cursor queryChildDocumentsForManage(
277            String parentDocumentId, String[] projection, String sortOrder)
278            throws FileNotFoundException {
279        throw new UnsupportedOperationException("Manage not supported");
280    }
281
282    /**
283     * Return documents that that match the given query under the requested
284     * root. The returned documents should be sorted by relevance in descending
285     * order. How documents are matched against the query string is an
286     * implementation detail left to each provider, but it's suggested that at
287     * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a
288     * case-insensitive fashion.
289     * <p>
290     * Only documents may be returned; directories are not supported in search
291     * results.
292     *
293     * @param rootId the root to search under.
294     * @param query string to match documents against.
295     * @param projection list of {@link Document} columns to put into the
296     *            cursor. If {@code null} all supported columns should be
297     *            included.
298     * @see DocumentsContract#EXTRA_LOADING
299     * @see DocumentsContract#EXTRA_INFO
300     * @see DocumentsContract#EXTRA_ERROR
301     */
302    @SuppressWarnings("unused")
303    public Cursor querySearchDocuments(String rootId, String query, String[] projection)
304            throws FileNotFoundException {
305        throw new UnsupportedOperationException("Search not supported");
306    }
307
308    /**
309     * Return concrete MIME type of the requested document. Must match the value
310     * of {@link Document#COLUMN_MIME_TYPE} for this document. The default
311     * implementation queries {@link #queryDocument(String, String[])}, so
312     * providers may choose to override this as an optimization.
313     */
314    public String getDocumentType(String documentId) throws FileNotFoundException {
315        final Cursor cursor = queryDocument(documentId, null);
316        try {
317            if (cursor.moveToFirst()) {
318                return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
319            } else {
320                return null;
321            }
322        } finally {
323            IoUtils.closeQuietly(cursor);
324        }
325    }
326
327    /**
328     * Open and return the requested document.
329     * <p>
330     * A provider should return a reliable {@link ParcelFileDescriptor} to
331     * detect when the remote caller has finished reading or writing the
332     * document. A provider may return a pipe or socket pair if the mode is
333     * exclusively {@link ParcelFileDescriptor#MODE_READ_ONLY} or
334     * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
335     * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
336     * disk.
337     * <p>
338     * If a provider blocks while downloading content, it should periodically
339     * check {@link CancellationSignal#isCanceled()} to abort abandoned open
340     * requests.
341     *
342     * @param documentId the document to return.
343     * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
344     * @param signal used by the caller to signal if the request should be
345     *            cancelled.
346     * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
347     *      OnCloseListener)
348     * @see ParcelFileDescriptor#createReliablePipe()
349     * @see ParcelFileDescriptor#createReliableSocketPair()
350     * @see ParcelFileDescriptor#parseMode(String)
351     */
352    public abstract ParcelFileDescriptor openDocument(
353            String documentId, String mode, CancellationSignal signal) throws FileNotFoundException;
354
355    /**
356     * Open and return a thumbnail of the requested document.
357     * <p>
358     * A provider should return a thumbnail closely matching the hinted size,
359     * attempting to serve from a local cache if possible. A provider should
360     * never return images more than double the hinted size.
361     * <p>
362     * If a provider performs expensive operations to download or generate a
363     * thumbnail, it should periodically check
364     * {@link CancellationSignal#isCanceled()} to abort abandoned thumbnail
365     * requests.
366     *
367     * @param documentId the document to return.
368     * @param sizeHint hint of the optimal thumbnail dimensions.
369     * @param signal used by the caller to signal if the request should be
370     *            cancelled.
371     * @see Document#FLAG_SUPPORTS_THUMBNAIL
372     */
373    @SuppressWarnings("unused")
374    public AssetFileDescriptor openDocumentThumbnail(
375            String documentId, Point sizeHint, CancellationSignal signal)
376            throws FileNotFoundException {
377        throw new UnsupportedOperationException("Thumbnails not supported");
378    }
379
380    /**
381     * Implementation is provided by the parent class. Cannot be overriden.
382     *
383     * @see #queryRoots(String[])
384     * @see #queryRecentDocuments(String, String[])
385     * @see #queryDocument(String, String[])
386     * @see #queryChildDocuments(String, String[], String)
387     * @see #querySearchDocuments(String, String, String[])
388     */
389    @Override
390    public final Cursor query(Uri uri, String[] projection, String selection,
391            String[] selectionArgs, String sortOrder) {
392        try {
393            switch (mMatcher.match(uri)) {
394                case MATCH_ROOTS:
395                    return queryRoots(projection);
396                case MATCH_RECENT:
397                    return queryRecentDocuments(getRootId(uri), projection);
398                case MATCH_SEARCH:
399                    return querySearchDocuments(
400                            getRootId(uri), getSearchDocumentsQuery(uri), projection);
401                case MATCH_DOCUMENT:
402                    return queryDocument(getDocumentId(uri), projection);
403                case MATCH_CHILDREN:
404                    if (DocumentsContract.isManageMode(uri)) {
405                        return queryChildDocumentsForManage(
406                                getDocumentId(uri), projection, sortOrder);
407                    } else {
408                        return queryChildDocuments(getDocumentId(uri), projection, sortOrder);
409                    }
410                default:
411                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
412            }
413        } catch (FileNotFoundException e) {
414            Log.w(TAG, "Failed during query", e);
415            return null;
416        }
417    }
418
419    /**
420     * Implementation is provided by the parent class. Cannot be overriden.
421     *
422     * @see #getDocumentType(String)
423     */
424    @Override
425    public final String getType(Uri uri) {
426        try {
427            switch (mMatcher.match(uri)) {
428                case MATCH_ROOT:
429                    return DocumentsContract.Root.MIME_TYPE_ITEM;
430                case MATCH_DOCUMENT:
431                    return getDocumentType(getDocumentId(uri));
432                default:
433                    return null;
434            }
435        } catch (FileNotFoundException e) {
436            Log.w(TAG, "Failed during getType", e);
437            return null;
438        }
439    }
440
441    /**
442     * Implementation is provided by the parent class. Throws by default, and
443     * cannot be overriden.
444     *
445     * @see #createDocument(String, String, String)
446     */
447    @Override
448    public final Uri insert(Uri uri, ContentValues values) {
449        throw new UnsupportedOperationException("Insert not supported");
450    }
451
452    /**
453     * Implementation is provided by the parent class. Throws by default, and
454     * cannot be overriden.
455     *
456     * @see #deleteDocument(String)
457     */
458    @Override
459    public final int delete(Uri uri, String selection, String[] selectionArgs) {
460        throw new UnsupportedOperationException("Delete not supported");
461    }
462
463    /**
464     * Implementation is provided by the parent class. Throws by default, and
465     * cannot be overriden.
466     */
467    @Override
468    public final int update(
469            Uri uri, ContentValues values, String selection, String[] selectionArgs) {
470        throw new UnsupportedOperationException("Update not supported");
471    }
472
473    /**
474     * Implementation is provided by the parent class. Can be overridden to
475     * provide additional functionality, but subclasses <em>must</em> always
476     * call the superclass. If the superclass returns {@code null}, the subclass
477     * may implement custom behavior.
478     *
479     * @see #openDocument(String, String, CancellationSignal)
480     * @see #deleteDocument(String)
481     */
482    @Override
483    public Bundle call(String method, String arg, Bundle extras) {
484        final Context context = getContext();
485
486        if (!method.startsWith("android:")) {
487            // Let non-platform methods pass through
488            return super.call(method, arg, extras);
489        }
490
491        final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
492        final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
493
494        // Require that caller can manage requested document
495        final boolean callerHasManage =
496                context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
497                == PackageManager.PERMISSION_GRANTED;
498        if (!callerHasManage) {
499            getContext().enforceCallingOrSelfUriPermission(
500                    documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method);
501        }
502
503        final Bundle out = new Bundle();
504        try {
505            if (METHOD_CREATE_DOCUMENT.equals(method)) {
506                final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
507                final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
508
509                final String newDocumentId = createDocument(documentId, mimeType, displayName);
510                out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);
511
512                // Extend permission grant towards caller if needed
513                if (!callerHasManage) {
514                    final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
515                            mAuthority, newDocumentId);
516                    context.grantUriPermission(getCallingPackage(), newDocumentUri,
517                            Intent.FLAG_GRANT_READ_URI_PERMISSION
518                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
519                            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
520                }
521
522            } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
523                deleteDocument(documentId);
524
525                // Document no longer exists, clean up any grants
526                context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
527                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
528                        | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
529
530            } else {
531                throw new UnsupportedOperationException("Method not supported " + method);
532            }
533        } catch (FileNotFoundException e) {
534            throw new IllegalStateException("Failed call " + method, e);
535        }
536        return out;
537    }
538
539    /**
540     * Implementation is provided by the parent class. Cannot be overriden.
541     *
542     * @see #openDocument(String, String, CancellationSignal)
543     */
544    @Override
545    public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
546        return openDocument(getDocumentId(uri), mode, null);
547    }
548
549    /**
550     * Implementation is provided by the parent class. Cannot be overriden.
551     *
552     * @see #openDocument(String, String, CancellationSignal)
553     */
554    @Override
555    public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
556            throws FileNotFoundException {
557        return openDocument(getDocumentId(uri), mode, signal);
558    }
559
560    /**
561     * Implementation is provided by the parent class. Cannot be overriden.
562     *
563     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
564     */
565    @Override
566    public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
567            throws FileNotFoundException {
568        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
569            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
570            return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
571        } else {
572            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
573        }
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(
583            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
584            throws FileNotFoundException {
585        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
586            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
587            return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);
588        } else {
589            return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
590        }
591    }
592}
593