DocumentsProvider.java revision aeb16e2435f9975b9fa1fc4b747796647a21292e
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.ACTION_DOCUMENT_ROOT_CHANGED;
20import static android.provider.DocumentsContract.EXTRA_AUTHORITY;
21import static android.provider.DocumentsContract.EXTRA_ROOTS;
22import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE;
23import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
24import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
25import static android.provider.DocumentsContract.METHOD_GET_ROOTS;
26import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT;
27import static android.provider.DocumentsContract.getDocId;
28import static android.provider.DocumentsContract.getSearchQuery;
29
30import android.content.ContentProvider;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.Intent;
34import android.content.UriMatcher;
35import android.content.pm.ProviderInfo;
36import android.content.res.AssetFileDescriptor;
37import android.database.Cursor;
38import android.graphics.Point;
39import android.net.Uri;
40import android.os.Bundle;
41import android.os.CancellationSignal;
42import android.os.ParcelFileDescriptor;
43import android.os.ParcelFileDescriptor.OnCloseListener;
44import android.provider.DocumentsContract.DocumentColumns;
45import android.provider.DocumentsContract.DocumentRoot;
46import android.provider.DocumentsContract.Documents;
47import android.util.Log;
48
49import libcore.io.IoUtils;
50
51import java.io.FileNotFoundException;
52import java.util.List;
53
54/**
55 * Base class for a document provider. A document provider should extend this
56 * class and implement the abstract methods.
57 * <p>
58 * Each document provider expresses one or more "roots" which each serve as the
59 * top-level of a tree. For example, a root could represent an account, or a
60 * physical storage device. Under each root, documents are referenced by
61 * {@link DocumentColumns#DOC_ID}, which must not change once returned.
62 * <p>
63 * Documents can be either an openable file (with a specific MIME type), or a
64 * directory containing additional documents (with the
65 * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
66 * capabilities, as described by {@link DocumentColumns#FLAGS}. The same
67 * {@link DocumentColumns#DOC_ID} can be included in multiple directories.
68 * <p>
69 * Document providers must be protected with the
70 * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
71 * only be requested by the system. The system-provided UI then issues narrow
72 * Uri permission grants for individual documents when the user explicitly picks
73 * documents.
74 *
75 * @see Intent#ACTION_OPEN_DOCUMENT
76 * @see Intent#ACTION_CREATE_DOCUMENT
77 */
78public abstract class DocumentsProvider extends ContentProvider {
79    private static final String TAG = "DocumentsProvider";
80
81    private static final int MATCH_DOCUMENT = 1;
82    private static final int MATCH_CHILDREN = 2;
83    private static final int MATCH_SEARCH = 3;
84
85    private String mAuthority;
86
87    private UriMatcher mMatcher;
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, "docs/*", MATCH_DOCUMENT);
95        mMatcher.addURI(mAuthority, "docs/*/children", MATCH_CHILDREN);
96        mMatcher.addURI(mAuthority, "docs/*/search", MATCH_SEARCH);
97
98        // Sanity check our setup
99        if (!info.exported) {
100            throw new SecurityException("Provider must be exported");
101        }
102        if (!info.grantUriPermissions) {
103            throw new SecurityException("Provider must grantUriPermissions");
104        }
105        if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
106                || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
107            throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
108        }
109
110        super.attachInfo(context, info);
111    }
112
113    /**
114     * Return list of all document roots provided by this document provider.
115     * When this list changes, a provider must call
116     * {@link #notifyDocumentRootsChanged()}.
117     */
118    public abstract List<DocumentRoot> getDocumentRoots();
119
120    /**
121     * Create and return a new document. A provider must allocate a new
122     * {@link DocumentColumns#DOC_ID} to represent the document, which must not
123     * change once returned.
124     *
125     * @param docId the parent directory to create the new document under.
126     * @param mimeType the MIME type associated with the new document.
127     * @param displayName the display name of the new document.
128     */
129    @SuppressWarnings("unused")
130    public String createDocument(String docId, String mimeType, String displayName)
131            throws FileNotFoundException {
132        throw new UnsupportedOperationException("Create not supported");
133    }
134
135    /**
136     * Rename the given document.
137     *
138     * @param docId the document to rename.
139     * @param displayName the new display name.
140     */
141    @SuppressWarnings("unused")
142    public void renameDocument(String docId, String displayName) throws FileNotFoundException {
143        throw new UnsupportedOperationException("Rename not supported");
144    }
145
146    /**
147     * Delete the given document.
148     *
149     * @param docId the document to delete.
150     */
151    @SuppressWarnings("unused")
152    public void deleteDocument(String docId) throws FileNotFoundException {
153        throw new UnsupportedOperationException("Delete not supported");
154    }
155
156    /**
157     * Return metadata for the given document. A provider should avoid making
158     * network requests to keep this request fast.
159     *
160     * @param docId the document to return.
161     */
162    public abstract Cursor queryDocument(String docId) throws FileNotFoundException;
163
164    /**
165     * Return the children of the given document which is a directory.
166     *
167     * @param docId the directory to return children for.
168     */
169    public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException;
170
171    /**
172     * Return documents that that match the given query, starting the search at
173     * the given directory.
174     *
175     * @param docId the directory to start search at.
176     */
177    @SuppressWarnings("unused")
178    public Cursor querySearch(String docId, String query) throws FileNotFoundException {
179        throw new UnsupportedOperationException("Search not supported");
180    }
181
182    /**
183     * Return MIME type for the given document. Must match the value of
184     * {@link DocumentColumns#MIME_TYPE} for this document.
185     */
186    public String getType(String docId) throws FileNotFoundException {
187        final Cursor cursor = queryDocument(docId);
188        try {
189            if (cursor.moveToFirst()) {
190                return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE));
191            } else {
192                return null;
193            }
194        } finally {
195            IoUtils.closeQuietly(cursor);
196        }
197    }
198
199    /**
200     * Open and return the requested document. A provider should return a
201     * reliable {@link ParcelFileDescriptor} to detect when the remote caller
202     * has finished reading or writing the document. A provider may return a
203     * pipe or socket pair if the mode is exclusively
204     * {@link ParcelFileDescriptor#MODE_READ_ONLY} or
205     * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
206     * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
207     * disk. If a provider blocks while downloading content, it should
208     * periodically check {@link CancellationSignal#isCanceled()} to abort
209     * abandoned open requests.
210     *
211     * @param docId the document to return.
212     * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
213     * @param signal used by the caller to signal if the request should be
214     *            cancelled.
215     * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
216     *      OnCloseListener)
217     * @see ParcelFileDescriptor#createReliablePipe()
218     * @see ParcelFileDescriptor#createReliableSocketPair()
219     */
220    public abstract ParcelFileDescriptor openDocument(
221            String docId, String mode, CancellationSignal signal) throws FileNotFoundException;
222
223    /**
224     * Open and return a thumbnail of the requested document. A provider should
225     * return a thumbnail closely matching the hinted size, attempting to serve
226     * from a local cache if possible. A provider should never return images
227     * more than double the hinted size. If a provider performs expensive
228     * operations to download or generate a thumbnail, it should periodically
229     * check {@link CancellationSignal#isCanceled()} to abort abandoned
230     * thumbnail requests.
231     *
232     * @param docId the document to return.
233     * @param sizeHint hint of the optimal thumbnail dimensions.
234     * @param signal used by the caller to signal if the request should be
235     *            cancelled.
236     * @see Documents#FLAG_SUPPORTS_THUMBNAIL
237     */
238    @SuppressWarnings("unused")
239    public AssetFileDescriptor openDocumentThumbnail(
240            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
241        throw new UnsupportedOperationException("Thumbnails not supported");
242    }
243
244    @Override
245    public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
246            String sortOrder) {
247        try {
248            switch (mMatcher.match(uri)) {
249                case MATCH_DOCUMENT:
250                    return queryDocument(getDocId(uri));
251                case MATCH_CHILDREN:
252                    return queryDocumentChildren(getDocId(uri));
253                case MATCH_SEARCH:
254                    return querySearch(getDocId(uri), getSearchQuery(uri));
255                default:
256                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
257            }
258        } catch (FileNotFoundException e) {
259            Log.w(TAG, "Failed during query", e);
260            return null;
261        }
262    }
263
264    @Override
265    public final String getType(Uri uri) {
266        try {
267            switch (mMatcher.match(uri)) {
268                case MATCH_DOCUMENT:
269                    return getType(getDocId(uri));
270                default:
271                    return null;
272            }
273        } catch (FileNotFoundException e) {
274            Log.w(TAG, "Failed during getType", e);
275            return null;
276        }
277    }
278
279    @Override
280    public final Uri insert(Uri uri, ContentValues values) {
281        throw new UnsupportedOperationException("Insert not supported");
282    }
283
284    @Override
285    public final int delete(Uri uri, String selection, String[] selectionArgs) {
286        throw new UnsupportedOperationException("Delete not supported");
287    }
288
289    @Override
290    public final int update(
291            Uri uri, ContentValues values, String selection, String[] selectionArgs) {
292        throw new UnsupportedOperationException("Update not supported");
293    }
294
295    @Override
296    public final Bundle callFromPackage(
297            String callingPackage, String method, String arg, Bundle extras) {
298        if (!method.startsWith("android:")) {
299            // Let non-platform methods pass through
300            return super.callFromPackage(callingPackage, method, arg, extras);
301        }
302
303        // Platform operations require the caller explicitly hold manage
304        // permission; Uri permissions don't extend management operations.
305        getContext().enforceCallingOrSelfPermission(
306                android.Manifest.permission.MANAGE_DOCUMENTS, "Document management");
307
308        final Bundle out = new Bundle();
309        try {
310            if (METHOD_GET_ROOTS.equals(method)) {
311                final List<DocumentRoot> roots = getDocumentRoots();
312                out.putParcelableList(EXTRA_ROOTS, roots);
313
314            } else if (METHOD_CREATE_DOCUMENT.equals(method)) {
315                final String docId = extras.getString(DocumentColumns.DOC_ID);
316                final String mimeType = extras.getString(DocumentColumns.MIME_TYPE);
317                final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
318
319                // TODO: issue Uri grant towards caller
320                final String newDocId = createDocument(docId, mimeType, displayName);
321                out.putString(DocumentColumns.DOC_ID, newDocId);
322
323            } else if (METHOD_RENAME_DOCUMENT.equals(method)) {
324                final String docId = extras.getString(DocumentColumns.DOC_ID);
325                final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
326                renameDocument(docId, displayName);
327
328            } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
329                final String docId = extras.getString(DocumentColumns.DOC_ID);
330                deleteDocument(docId);
331
332            } else {
333                throw new UnsupportedOperationException("Method not supported " + method);
334            }
335        } catch (FileNotFoundException e) {
336            throw new IllegalStateException("Failed call " + method, e);
337        }
338        return out;
339    }
340
341    @Override
342    public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
343        return openDocument(getDocId(uri), mode, null);
344    }
345
346    @Override
347    public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
348            throws FileNotFoundException {
349        return openDocument(getDocId(uri), mode, signal);
350    }
351
352    @Override
353    public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
354            throws FileNotFoundException {
355        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
356            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
357            return openDocumentThumbnail(getDocId(uri), sizeHint, null);
358        } else {
359            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
360        }
361    }
362
363    @Override
364    public final AssetFileDescriptor openTypedAssetFile(
365            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
366            throws FileNotFoundException {
367        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
368            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
369            return openDocumentThumbnail(getDocId(uri), sizeHint, signal);
370        } else {
371            return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
372        }
373    }
374
375    /**
376     * Notify system that {@link #getDocumentRoots()} has changed, usually due to an
377     * account or device change.
378     */
379    public void notifyDocumentRootsChanged() {
380        final Intent intent = new Intent(ACTION_DOCUMENT_ROOT_CHANGED);
381        intent.putExtra(EXTRA_AUTHORITY, mAuthority);
382        getContext().sendBroadcast(intent);
383    }
384}
385