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