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