DocumentsContract.java revision 9d0843df7e3984293dc4ab6ee2f9502e898b63aa
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 android.content.ContentProvider;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ProviderInfo;
26import android.content.res.AssetFileDescriptor;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.BitmapFactory;
30import android.graphics.Point;
31import android.net.Uri;
32import android.os.Bundle;
33import android.util.Log;
34
35import com.google.android.collect.Lists;
36
37import libcore.io.IoUtils;
38
39import java.io.FileDescriptor;
40import java.io.IOException;
41import java.io.InputStream;
42import java.util.List;
43
44/**
45 * The contract between a storage backend and the platform. Contains definitions
46 * for the supported URIs and columns.
47 */
48public final class DocumentsContract {
49    private static final String TAG = "Documents";
50
51    // content://com.example/roots/
52    // content://com.example/roots/sdcard/
53    // content://com.example/roots/sdcard/docs/0/
54    // content://com.example/roots/sdcard/docs/0/contents/
55    // content://com.example/roots/sdcard/docs/0/search/?query=pony
56
57    /** {@hide} */
58    public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
59
60    /** {@hide} */
61    public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED";
62
63    public static class Documents {
64        private Documents() {
65        }
66
67        /**
68         * MIME type of a document which is a directory that may contain additional
69         * documents.
70         *
71         * @see #buildContentsUri(String, String, String)
72         */
73        public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
74
75        /**
76         * {@link DocumentColumns#DOC_ID} value representing the root directory of a
77         * storage root.
78         */
79        public static final String DOC_ID_ROOT = "0";
80
81        /**
82         * Flag indicating that a document is a directory that supports creation of
83         * new files within it.
84         *
85         * @see DocumentColumns#FLAGS
86         * @see #createDocument(ContentResolver, Uri, String, String)
87         */
88        public static final int FLAG_SUPPORTS_CREATE = 1;
89
90        /**
91         * Flag indicating that a document is renamable.
92         *
93         * @see DocumentColumns#FLAGS
94         * @see #renameDocument(ContentResolver, Uri, String)
95         */
96        public static final int FLAG_SUPPORTS_RENAME = 1 << 1;
97
98        /**
99         * Flag indicating that a document is deletable.
100         *
101         * @see DocumentColumns#FLAGS
102         */
103        public static final int FLAG_SUPPORTS_DELETE = 1 << 2;
104
105        /**
106         * Flag indicating that a document can be represented as a thumbnail.
107         *
108         * @see DocumentColumns#FLAGS
109         * @see #getThumbnail(ContentResolver, Uri, Point)
110         */
111        public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;
112
113        /**
114         * Flag indicating that a document is a directory that supports search.
115         *
116         * @see DocumentColumns#FLAGS
117         */
118        public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
119
120        /**
121         * Flag indicating that a document is writable.
122         *
123         * @see DocumentColumns#FLAGS
124         */
125        public static final int FLAG_SUPPORTS_WRITE = 1 << 5;
126
127        /**
128         * Flag indicating that a document is a directory that prefers its contents
129         * be shown in a larger format grid. Usually suitable when a directory
130         * contains mostly pictures.
131         *
132         * @see DocumentColumns#FLAGS
133         */
134        public static final int FLAG_PREFERS_GRID = 1 << 6;
135    }
136
137    /**
138     * Optimal dimensions for a document thumbnail request, stored as a
139     * {@link Point} object. This is only a hint, and the returned thumbnail may
140     * have different dimensions.
141     *
142     * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
143     */
144    public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
145
146    /**
147     * Extra boolean flag included in a directory {@link Cursor#getExtras()}
148     * indicating that the backend can provide additional data if requested,
149     * such as additional search results.
150     */
151    public static final String EXTRA_HAS_MORE = "has_more";
152
153    /**
154     * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
155     * directory to request that additional data should be fetched. When
156     * requested data is ready, the provider should send a change notification
157     * to cause a requery.
158     *
159     * @see Cursor#respond(Bundle)
160     * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
161     *      boolean)
162     */
163    public static final String EXTRA_REQUEST_MORE = "request_more";
164
165    private static final String PATH_ROOTS = "roots";
166    private static final String PATH_DOCS = "docs";
167    private static final String PATH_CONTENTS = "contents";
168    private static final String PATH_SEARCH = "search";
169
170    private static final String PARAM_QUERY = "query";
171    private static final String PARAM_LOCAL_ONLY = "localOnly";
172
173    /**
174     * Build URI representing the roots in a storage backend.
175     */
176    public static Uri buildRootsUri(String authority) {
177        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
178                .authority(authority).appendPath(PATH_ROOTS).build();
179    }
180
181    public static Uri buildRootUri(String authority, String rootId) {
182        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
183                .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
184    }
185
186    /**
187     * Build URI representing the given {@link DocumentColumns#DOC_ID} in a
188     * storage root.
189     */
190    public static Uri buildDocumentUri(String authority, String rootId, String docId) {
191        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
192                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
193                .build();
194    }
195
196    /**
197     * Build URI representing the contents of the given directory in a storage
198     * backend. The given document must be {@link Documents#MIME_TYPE_DIR}.
199     */
200    public static Uri buildContentsUri(String authority, String rootId, String docId) {
201        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
202                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
203                .appendPath(PATH_CONTENTS).build();
204    }
205
206    /**
207     * Build URI representing a search for matching documents under a directory
208     * in a storage backend.
209     */
210    public static Uri buildSearchUri(String authority, String rootId, String docId, String query) {
211        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
212                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
213                .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
214    }
215
216    public static Uri buildDocumentUri(Uri relatedUri, String docId) {
217        return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId);
218    }
219
220    public static Uri buildContentsUri(Uri relatedUri) {
221        return buildContentsUri(
222                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri));
223    }
224
225    public static Uri buildSearchUri(Uri relatedUri, String query) {
226        return buildSearchUri(
227                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query);
228    }
229
230    public static String getRootId(Uri documentUri) {
231        final List<String> paths = documentUri.getPathSegments();
232        if (paths.size() < 2) {
233            throw new IllegalArgumentException("Not a root: " + documentUri);
234        }
235        if (!PATH_ROOTS.equals(paths.get(0))) {
236            throw new IllegalArgumentException("Not a root: " + documentUri);
237        }
238        return paths.get(1);
239    }
240
241    public static String getDocId(Uri documentUri) {
242        final List<String> paths = documentUri.getPathSegments();
243        if (paths.size() < 4) {
244            throw new IllegalArgumentException("Not a document: " + documentUri);
245        }
246        if (!PATH_ROOTS.equals(paths.get(0))) {
247            throw new IllegalArgumentException("Not a document: " + documentUri);
248        }
249        if (!PATH_DOCS.equals(paths.get(2))) {
250            throw new IllegalArgumentException("Not a document: " + documentUri);
251        }
252        return paths.get(3);
253    }
254
255    /**
256     * Return requested search query from the given Uri.
257     */
258    public static String getSearchQuery(Uri documentUri) {
259        return documentUri.getQueryParameter(PARAM_QUERY);
260    }
261
262    /**
263     * Mark the given Uri to indicate that only locally-available contents
264     * should be returned.
265     */
266    public static Uri setLocalOnly(Uri documentUri) {
267        return documentUri.buildUpon()
268                .appendQueryParameter(PARAM_LOCAL_ONLY, String.valueOf(true)).build();
269    }
270
271    /**
272     * Return if the given Uri is requesting that only locally-available content
273     * be returned. That is, no network connections should be initiated to
274     * provide the metadata or content.
275     */
276    public static boolean isLocalOnly(Uri documentUri) {
277        return documentUri.getBooleanQueryParameter(PARAM_LOCAL_ONLY, false);
278    }
279
280    /**
281     * These are standard columns for document URIs. Storage backend providers
282     * <em>must</em> support at least these columns when queried.
283     *
284     * @see Intent#ACTION_OPEN_DOCUMENT
285     * @see Intent#ACTION_CREATE_DOCUMENT
286     */
287    public interface DocumentColumns extends OpenableColumns {
288        /**
289         * The ID for a document under a storage backend root. Values
290         * <em>must</em> never change once returned. This field is read-only to
291         * document clients.
292         * <p>
293         * Type: STRING
294         */
295        public static final String DOC_ID = "doc_id";
296
297        /**
298         * MIME type of a document, matching the value returned by
299         * {@link ContentResolver#getType(android.net.Uri)}. This field must be
300         * provided when a new document is created, but after that the field is
301         * read-only.
302         * <p>
303         * Type: STRING
304         *
305         * @see Documents#MIME_TYPE_DIR
306         */
307        public static final String MIME_TYPE = "mime_type";
308
309        /**
310         * Timestamp when a document was last modified, in milliseconds since
311         * January 1, 1970 00:00:00.0 UTC. This field is read-only to document
312         * clients.
313         * <p>
314         * Type: INTEGER (long)
315         *
316         * @see System#currentTimeMillis()
317         */
318        public static final String LAST_MODIFIED = "last_modified";
319
320        /**
321         * Flags that apply to a specific document. This field is read-only to
322         * document clients.
323         * <p>
324         * Type: INTEGER (int)
325         */
326        public static final String FLAGS = "flags";
327
328        /**
329         * Summary for this document, or {@code null} to omit.
330         * <p>
331         * Type: STRING
332         */
333        public static final String SUMMARY = "summary";
334    }
335
336    public static class Roots {
337        private Roots() {
338        }
339
340        public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
341        public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
342
343        /**
344         * Root that represents a cloud-based storage service.
345         *
346         * @see RootColumns#ROOT_TYPE
347         */
348        public static final int ROOT_TYPE_SERVICE = 1;
349
350        /**
351         * Root that represents a shortcut to content that may be available
352         * elsewhere through another storage root.
353         *
354         * @see RootColumns#ROOT_TYPE
355         */
356        public static final int ROOT_TYPE_SHORTCUT = 2;
357
358        /**
359         * Root that represents a physical storage device.
360         *
361         * @see RootColumns#ROOT_TYPE
362         */
363        public static final int ROOT_TYPE_DEVICE = 3;
364
365        /**
366         * Root that represents a physical storage device that should only be
367         * displayed to advanced users.
368         *
369         * @see RootColumns#ROOT_TYPE
370         */
371        public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
372    }
373
374    /**
375     * These are standard columns for the roots URI.
376     *
377     * @see DocumentsContract#buildRootsUri(String)
378     */
379    public interface RootColumns {
380        public static final String ROOT_ID = "root_id";
381
382        /**
383         * Storage root type, use for clustering.
384         * <p>
385         * Type: INTEGER (int)
386         *
387         * @see Roots#ROOT_TYPE_SERVICE
388         * @see Roots#ROOT_TYPE_DEVICE
389         */
390        public static final String ROOT_TYPE = "root_type";
391
392        /**
393         * Icon resource ID for this storage root, or {@code 0} to use the
394         * default {@link ProviderInfo#icon}.
395         * <p>
396         * Type: INTEGER (int)
397         */
398        public static final String ICON = "icon";
399
400        /**
401         * Title for this storage root, or {@code null} to use the default
402         * {@link ProviderInfo#labelRes}.
403         * <p>
404         * Type: STRING
405         */
406        public static final String TITLE = "title";
407
408        /**
409         * Summary for this storage root, or {@code null} to omit.
410         * <p>
411         * Type: STRING
412         */
413        public static final String SUMMARY = "summary";
414
415        /**
416         * Number of free bytes of available in this storage root, or -1 if
417         * unknown or unbounded.
418         * <p>
419         * Type: INTEGER (long)
420         */
421        public static final String AVAILABLE_BYTES = "available_bytes";
422    }
423
424    /**
425     * Return list of all documents that the calling package has "open." These
426     * are Uris matching {@link DocumentsContract} to which persistent
427     * read/write access has been granted, usually through
428     * {@link Intent#ACTION_OPEN_DOCUMENT} or
429     * {@link Intent#ACTION_CREATE_DOCUMENT}.
430     *
431     * @see Context#grantUriPermission(String, Uri, int)
432     * @see ContentResolver#getIncomingUriPermissionGrants(int, int)
433     */
434    public static Uri[] getOpenDocuments(Context context) {
435        final int openedFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
436                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION;
437        final Uri[] uris = context.getContentResolver()
438                .getIncomingUriPermissionGrants(openedFlags, openedFlags);
439
440        // Filter to only include document providers
441        final PackageManager pm = context.getPackageManager();
442        final List<Uri> result = Lists.newArrayList();
443        for (Uri uri : uris) {
444            final ProviderInfo info = pm.resolveContentProvider(
445                    uri.getAuthority(), PackageManager.GET_META_DATA);
446            if (info.metaData.containsKey(META_DATA_DOCUMENT_PROVIDER)) {
447                result.add(uri);
448            }
449        }
450
451        return result.toArray(new Uri[result.size()]);
452    }
453
454    /**
455     * Return thumbnail representing the document at the given URI. Callers are
456     * responsible for their own caching. Given document must have
457     * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set.
458     *
459     * @return decoded thumbnail, or {@code null} if problem was encountered.
460     */
461    public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
462        final Bundle opts = new Bundle();
463        opts.putParcelable(EXTRA_THUMBNAIL_SIZE, size);
464
465        AssetFileDescriptor afd = null;
466        try {
467            afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts);
468
469            final FileDescriptor fd = afd.getFileDescriptor();
470            final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
471
472            bitmapOpts.inJustDecodeBounds = true;
473            BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts);
474
475            final int widthSample = bitmapOpts.outWidth / size.x;
476            final int heightSample = bitmapOpts.outHeight / size.y;
477
478            bitmapOpts.inJustDecodeBounds = false;
479            bitmapOpts.inSampleSize = Math.min(widthSample, heightSample);
480            return BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts);
481        } catch (IOException e) {
482            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
483            return null;
484        } finally {
485            IoUtils.closeQuietly(afd);
486        }
487    }
488
489    /**
490     * Create a new document under a specific parent document with the given
491     * display name and MIME type.
492     *
493     * @param parentDocumentUri document with
494     *            {@link Documents#FLAG_SUPPORTS_CREATE}
495     * @param displayName name for new document
496     * @param mimeType MIME type for new document, which cannot be changed
497     * @return newly created document Uri, or {@code null} if failed
498     */
499    public static Uri createDocument(
500            ContentResolver resolver, Uri parentDocumentUri, String displayName, String mimeType) {
501        final ContentValues values = new ContentValues();
502        values.put(DocumentColumns.MIME_TYPE, mimeType);
503        values.put(DocumentColumns.DISPLAY_NAME, displayName);
504        return resolver.insert(parentDocumentUri, values);
505    }
506
507    /**
508     * Rename the document at the given URI. Given document must have
509     * {@link Documents#FLAG_SUPPORTS_RENAME} set.
510     *
511     * @return if rename was successful.
512     */
513    public static boolean renameDocument(
514            ContentResolver resolver, Uri documentUri, String displayName) {
515        final ContentValues values = new ContentValues();
516        values.put(DocumentColumns.DISPLAY_NAME, displayName);
517        return (resolver.update(documentUri, values, null, null) == 1);
518    }
519
520    /**
521     * Notify the system that roots have changed for the given storage provider.
522     * This signal is used to invalidate internal caches.
523     */
524    public static void notifyRootsChanged(Context context, String authority) {
525        final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED);
526        intent.setData(buildRootsUri(authority));
527        context.sendBroadcast(intent);
528    }
529}
530