DocumentsProvider.java revision 4ec973925fc2cd18f9ec0d0ca5af588564fded27
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.ContentValues;
28import android.content.Context;
29import android.content.Intent;
30import android.content.UriMatcher;
31import android.content.pm.PackageManager;
32import android.content.pm.ProviderInfo;
33import android.content.res.AssetFileDescriptor;
34import android.database.Cursor;
35import android.graphics.Point;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.CancellationSignal;
39import android.os.ParcelFileDescriptor;
40import android.os.ParcelFileDescriptor.OnCloseListener;
41import android.provider.DocumentsContract.Document;
42import android.util.Log;
43
44import libcore.io.IoUtils;
45
46import java.io.FileNotFoundException;
47
48/**
49 * Base class for a document provider. A document provider should extend this
50 * class and implement the abstract methods.
51 * <p>
52 * Each document provider expresses one or more "roots" which each serve as the
53 * top-level of a tree. For example, a root could represent an account, or a
54 * physical storage device. Under each root, documents are referenced by
55 * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned.
56 * <p>
57 * Documents can be either an openable file (with a specific MIME type), or a
58 * directory containing additional documents (with the
59 * {@link Document#MIME_TYPE_DIR} MIME type). Each document can have different
60 * capabilities, as described by {@link Document#COLUMN_FLAGS}. The same
61 * {@link Document#COLUMN_DOCUMENT_ID} can be included in multiple directories.
62 * <p>
63 * Document providers must be protected with the
64 * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
65 * only be requested by the system. The system-provided UI then issues narrow
66 * Uri permission grants for individual documents when the user explicitly picks
67 * documents.
68 *
69 * @see Intent#ACTION_OPEN_DOCUMENT
70 * @see Intent#ACTION_CREATE_DOCUMENT
71 */
72public abstract class DocumentsProvider extends ContentProvider {
73    private static final String TAG = "DocumentsProvider";
74
75    private static final int MATCH_ROOTS = 1;
76    private static final int MATCH_ROOT = 2;
77    private static final int MATCH_RECENT = 3;
78    private static final int MATCH_DOCUMENT = 4;
79    private static final int MATCH_CHILDREN = 5;
80    private static final int MATCH_SEARCH = 6;
81
82    private String mAuthority;
83
84    private UriMatcher mMatcher;
85
86    /**
87     * Implementation is provided by the parent class.
88     */
89    @Override
90    public void attachInfo(Context context, ProviderInfo info) {
91        mAuthority = info.authority;
92
93        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
94        mMatcher.addURI(mAuthority, "root", MATCH_ROOTS);
95        mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT);
96        mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT);
97        mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
98        mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
99        mMatcher.addURI(mAuthority, "document/*/search", MATCH_SEARCH);
100
101        // Sanity check our setup
102        if (!info.exported) {
103            throw new SecurityException("Provider must be exported");
104        }
105        if (!info.grantUriPermissions) {
106            throw new SecurityException("Provider must grantUriPermissions");
107        }
108        if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
109                || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
110            throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
111        }
112
113        super.attachInfo(context, info);
114    }
115
116    /**
117     * Create a new document and return its {@link Document#COLUMN_DOCUMENT_ID}.
118     * A provider must allocate a new {@link Document#COLUMN_DOCUMENT_ID} to
119     * represent the document, which must not change once returned.
120     *
121     * @param documentId the parent directory to create the new document under.
122     * @param mimeType the MIME type associated with the new document.
123     * @param displayName the display name of the new document.
124     */
125    @SuppressWarnings("unused")
126    public String createDocument(String documentId, String mimeType, String displayName)
127            throws FileNotFoundException {
128        throw new UnsupportedOperationException("Create not supported");
129    }
130
131    /**
132     * Delete the given document. Upon returning, any Uri permission grants for
133     * the given document will be revoked. If additional documents were deleted
134     * as a side effect of this call, such as documents inside a directory, the
135     * implementor is responsible for revoking those permissions.
136     *
137     * @param documentId the document to delete.
138     */
139    @SuppressWarnings("unused")
140    public void deleteDocument(String documentId) throws FileNotFoundException {
141        throw new UnsupportedOperationException("Delete not supported");
142    }
143
144    public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException;
145
146    @SuppressWarnings("unused")
147    public Cursor queryRecentDocuments(String rootId, String[] projection)
148            throws FileNotFoundException {
149        throw new UnsupportedOperationException("Recent not supported");
150    }
151
152    /**
153     * Return metadata for the given document. A provider should avoid making
154     * network requests to keep this request fast.
155     *
156     * @param documentId the document to return.
157     */
158    public abstract Cursor queryDocument(String documentId, String[] projection)
159            throws FileNotFoundException;
160
161    /**
162     * Return the children of the given document which is a directory.
163     *
164     * @param parentDocumentId the directory to return children for.
165     */
166    public abstract Cursor queryChildDocuments(
167            String parentDocumentId, String[] projection, String sortOrder)
168            throws FileNotFoundException;
169
170    /** {@hide} */
171    @SuppressWarnings("unused")
172    public Cursor queryChildDocumentsForManage(
173            String parentDocumentId, String[] projection, String sortOrder)
174            throws FileNotFoundException {
175        throw new UnsupportedOperationException("Manage not supported");
176    }
177
178    /**
179     * Return documents that that match the given query, starting the search at
180     * the given directory.
181     *
182     * @param parentDocumentId the directory to start search at.
183     */
184    @SuppressWarnings("unused")
185    public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection)
186            throws FileNotFoundException {
187        throw new UnsupportedOperationException("Search not supported");
188    }
189
190    /**
191     * Return MIME type for the given document. Must match the value of
192     * {@link Document#COLUMN_MIME_TYPE} for this document.
193     */
194    public String getDocumentType(String documentId) throws FileNotFoundException {
195        final Cursor cursor = queryDocument(documentId, null);
196        try {
197            if (cursor.moveToFirst()) {
198                return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
199            } else {
200                return null;
201            }
202        } finally {
203            IoUtils.closeQuietly(cursor);
204        }
205    }
206
207    /**
208     * Open and return the requested document. A provider should return a
209     * reliable {@link ParcelFileDescriptor} to detect when the remote caller
210     * has finished reading or writing the document. A provider may return a
211     * pipe or socket pair if the mode is exclusively
212     * {@link ParcelFileDescriptor#MODE_READ_ONLY} or
213     * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
214     * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
215     * disk. If a provider blocks while downloading content, it should
216     * periodically check {@link CancellationSignal#isCanceled()} to abort
217     * abandoned open requests.
218     *
219     * @param docId the document to return.
220     * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
221     * @param signal used by the caller to signal if the request should be
222     *            cancelled.
223     * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
224     *      OnCloseListener)
225     * @see ParcelFileDescriptor#createReliablePipe()
226     * @see ParcelFileDescriptor#createReliableSocketPair()
227     */
228    public abstract ParcelFileDescriptor openDocument(
229            String docId, String mode, CancellationSignal signal) throws FileNotFoundException;
230
231    /**
232     * Open and return a thumbnail of the requested document. A provider should
233     * return a thumbnail closely matching the hinted size, attempting to serve
234     * from a local cache if possible. A provider should never return images
235     * more than double the hinted size. If a provider performs expensive
236     * operations to download or generate a thumbnail, it should periodically
237     * check {@link CancellationSignal#isCanceled()} to abort abandoned
238     * thumbnail requests.
239     *
240     * @param docId the document to return.
241     * @param sizeHint hint of the optimal thumbnail dimensions.
242     * @param signal used by the caller to signal if the request should be
243     *            cancelled.
244     * @see Document#FLAG_SUPPORTS_THUMBNAIL
245     */
246    @SuppressWarnings("unused")
247    public AssetFileDescriptor openDocumentThumbnail(
248            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
249        throw new UnsupportedOperationException("Thumbnails not supported");
250    }
251
252    /**
253     * Implementation is provided by the parent class. Cannot be overriden.
254     *
255     * @see #queryRoots(String[])
256     * @see #queryRecentDocuments(String, String[])
257     * @see #queryDocument(String, String[])
258     * @see #queryChildDocuments(String, String[], String)
259     * @see #querySearchDocuments(String, String, String[])
260     */
261    @Override
262    public final Cursor query(Uri uri, String[] projection, String selection,
263            String[] selectionArgs, String sortOrder) {
264        try {
265            switch (mMatcher.match(uri)) {
266                case MATCH_ROOTS:
267                    return queryRoots(projection);
268                case MATCH_RECENT:
269                    return queryRecentDocuments(getRootId(uri), projection);
270                case MATCH_DOCUMENT:
271                    return queryDocument(getDocumentId(uri), projection);
272                case MATCH_CHILDREN:
273                    if (DocumentsContract.isManageMode(uri)) {
274                        return queryChildDocumentsForManage(
275                                getDocumentId(uri), projection, sortOrder);
276                    } else {
277                        return queryChildDocuments(getDocumentId(uri), projection, sortOrder);
278                    }
279                case MATCH_SEARCH:
280                    return querySearchDocuments(
281                            getDocumentId(uri), getSearchDocumentsQuery(uri), projection);
282                default:
283                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
284            }
285        } catch (FileNotFoundException e) {
286            Log.w(TAG, "Failed during query", e);
287            return null;
288        }
289    }
290
291    /**
292     * Implementation is provided by the parent class. Cannot be overriden.
293     *
294     * @see #getDocumentType(String)
295     */
296    @Override
297    public final String getType(Uri uri) {
298        try {
299            switch (mMatcher.match(uri)) {
300                case MATCH_ROOT:
301                    return DocumentsContract.Root.MIME_TYPE_ITEM;
302                case MATCH_DOCUMENT:
303                    return getDocumentType(getDocumentId(uri));
304                default:
305                    return null;
306            }
307        } catch (FileNotFoundException e) {
308            Log.w(TAG, "Failed during getType", e);
309            return null;
310        }
311    }
312
313    /**
314     * Implementation is provided by the parent class. Throws by default, and
315     * cannot be overriden.
316     *
317     * @see #createDocument(String, String, String)
318     */
319    @Override
320    public final Uri insert(Uri uri, ContentValues values) {
321        throw new UnsupportedOperationException("Insert not supported");
322    }
323
324    /**
325     * Implementation is provided by the parent class. Throws by default, and
326     * cannot be overriden.
327     *
328     * @see #deleteDocument(String)
329     */
330    @Override
331    public final int delete(Uri uri, String selection, String[] selectionArgs) {
332        throw new UnsupportedOperationException("Delete not supported");
333    }
334
335    /**
336     * Implementation is provided by the parent class. Throws by default, and
337     * cannot be overriden.
338     */
339    @Override
340    public final int update(
341            Uri uri, ContentValues values, String selection, String[] selectionArgs) {
342        throw new UnsupportedOperationException("Update not supported");
343    }
344
345    /**
346     * Implementation is provided by the parent class. Can be overridden to
347     * provide additional functionality, but subclasses <em>must</em> always
348     * call the superclass. If the superclass returns {@code null}, the subclass
349     * may implement custom behavior.
350     *
351     * @see #openDocument(String, String, CancellationSignal)
352     * @see #deleteDocument(String)
353     */
354    @Override
355    public Bundle call(String method, String arg, Bundle extras) {
356        final Context context = getContext();
357
358        if (!method.startsWith("android:")) {
359            // Let non-platform methods pass through
360            return super.call(method, arg, extras);
361        }
362
363        final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
364        final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
365
366        // Require that caller can manage given document
367        final boolean callerHasManage =
368                context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
369                == PackageManager.PERMISSION_GRANTED;
370        if (!callerHasManage) {
371            getContext().enforceCallingOrSelfUriPermission(
372                    documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method);
373        }
374
375        final Bundle out = new Bundle();
376        try {
377            if (METHOD_CREATE_DOCUMENT.equals(method)) {
378                final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
379                final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
380
381                final String newDocumentId = createDocument(documentId, mimeType, displayName);
382                out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);
383
384                // Extend permission grant towards caller if needed
385                if (!callerHasManage) {
386                    final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
387                            mAuthority, newDocumentId);
388                    context.grantUriPermission(getCallingPackage(), newDocumentUri,
389                            Intent.FLAG_GRANT_READ_URI_PERMISSION
390                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
391                            | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
392                }
393
394            } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
395                deleteDocument(documentId);
396
397                // Document no longer exists, clean up any grants
398                context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
399                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
400                        | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
401
402            } else {
403                throw new UnsupportedOperationException("Method not supported " + method);
404            }
405        } catch (FileNotFoundException e) {
406            throw new IllegalStateException("Failed call " + method, e);
407        }
408        return out;
409    }
410
411    /**
412     * Implementation is provided by the parent class.
413     *
414     * @see #openDocument(String, String, CancellationSignal)
415     */
416    @Override
417    public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
418        return openDocument(getDocumentId(uri), mode, null);
419    }
420
421    /**
422     * Implementation is provided by the parent class.
423     *
424     * @see #openDocument(String, String, CancellationSignal)
425     */
426    @Override
427    public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
428            throws FileNotFoundException {
429        return openDocument(getDocumentId(uri), mode, signal);
430    }
431
432    /**
433     * Implementation is provided by the parent class.
434     *
435     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
436     */
437    @Override
438    public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
439            throws FileNotFoundException {
440        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
441            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
442            return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
443        } else {
444            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
445        }
446    }
447
448    /**
449     * Implementation is provided by the parent class.
450     *
451     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
452     */
453    @Override
454    public final AssetFileDescriptor openTypedAssetFile(
455            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
456            throws FileNotFoundException {
457        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
458            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
459            return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);
460        } else {
461            return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
462        }
463    }
464}
465