DocumentsContract.java revision 08da7a1143b0c9cfb703971d882e0886bbd7d9de
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.IOException;
40import java.io.InputStream;
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    /**
57     * MIME type of a document which is a directory that may contain additional
58     * documents.
59     *
60     * @see #buildContentsUri(Uri)
61     */
62    public static final String MIME_TYPE_DIRECTORY = "vnd.android.cursor.dir/doc";
63
64    /** {@hide} */
65    public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
66
67    /**
68     * {@link DocumentColumns#DOC_ID} value representing the root directory of a
69     * storage root.
70     */
71    public static final String ROOT_DOC_ID = "0";
72
73    /**
74     * Flag indicating that a document is a directory that supports creation of
75     * new files within it.
76     *
77     * @see DocumentColumns#FLAGS
78     * @see #buildContentsUri(Uri)
79     */
80    public static final int FLAG_SUPPORTS_CREATE = 1;
81
82    /**
83     * Flag indicating that a document is renamable.
84     *
85     * @see DocumentColumns#FLAGS
86     * @see #renameDocument(ContentResolver, Uri, String)
87     */
88    public static final int FLAG_SUPPORTS_RENAME = 1 << 1;
89
90    /**
91     * Flag indicating that a document is deletable.
92     *
93     * @see DocumentColumns#FLAGS
94     */
95    public static final int FLAG_SUPPORTS_DELETE = 1 << 2;
96
97    /**
98     * Flag indicating that a document can be represented as a thumbnail.
99     *
100     * @see DocumentColumns#FLAGS
101     * @see #getThumbnail(ContentResolver, Uri, Point)
102     */
103    public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;
104
105    /**
106     * Flag indicating that a document is a directory that supports search.
107     *
108     * @see DocumentColumns#FLAGS
109     */
110    public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
111
112    // TODO: flag indicating that document is writable?
113
114    /**
115     * Optimal dimensions for a document thumbnail request, stored as a
116     * {@link Point} object. This is only a hint, and the returned thumbnail may
117     * have different dimensions.
118     *
119     * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
120     */
121    public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
122
123    /**
124     * Extra boolean flag included in a directory {@link Cursor#getExtras()}
125     * indicating that the backend can provide additional data if requested,
126     * such as additional search results.
127     */
128    public static final String EXTRA_HAS_MORE = "has_more";
129
130    /**
131     * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
132     * directory to request that additional data should be fetched. When
133     * requested data is ready, the provider should send a change notification
134     * to cause a requery.
135     *
136     * @see Cursor#respond(Bundle)
137     * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
138     *      boolean)
139     */
140    public static final String EXTRA_REQUEST_MORE = "request_more";
141
142    private static final String PATH_ROOTS = "roots";
143    private static final String PATH_DOCS = "docs";
144    private static final String PATH_CONTENTS = "contents";
145    private static final String PATH_SEARCH = "search";
146
147    public static final String PARAM_QUERY = "query";
148
149    /**
150     * Build URI representing the roots in a storage backend.
151     */
152    public static Uri buildRootsUri(String authority) {
153        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
154                .authority(authority).appendPath(PATH_ROOTS).build();
155    }
156
157    public static Uri buildRootUri(String authority, String rootId) {
158        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
159                .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
160    }
161
162    /**
163     * Build URI representing the given {@link DocumentColumns#DOC_ID} in a
164     * storage root.
165     */
166    public static Uri buildDocumentUri(String authority, String rootId, String docId) {
167        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
168                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
169                .build();
170    }
171
172    /**
173     * Build URI representing the contents of the given directory in a storage
174     * backend. The given document must be {@link #MIME_TYPE_DIRECTORY}.
175     */
176    public static Uri buildContentsUri(String authority, String rootId, String docId) {
177        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
178                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
179                .appendPath(PATH_CONTENTS).build();
180    }
181
182    /**
183     * Build URI representing a search for matching documents under a directory
184     * in a storage backend.
185     */
186    public static Uri buildSearchUri(String authority, String rootId, String docId, String query) {
187        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
188                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
189                .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
190    }
191
192    public static Uri buildDocumentUri(Uri relatedUri, String docId) {
193        return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId);
194    }
195
196    public static Uri buildContentsUri(Uri relatedUri) {
197        return buildContentsUri(
198                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri));
199    }
200
201    public static Uri buildSearchUri(Uri relatedUri, String query) {
202        return buildSearchUri(
203                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query);
204    }
205
206    public static String getRootId(Uri documentUri) {
207        final List<String> paths = documentUri.getPathSegments();
208        if (paths.size() < 2) {
209            throw new IllegalArgumentException("Not a root: " + documentUri);
210        }
211        if (!PATH_ROOTS.equals(paths.get(0))) {
212            throw new IllegalArgumentException("Not a root: " + documentUri);
213        }
214        return paths.get(1);
215    }
216
217    public static String getDocId(Uri documentUri) {
218        final List<String> paths = documentUri.getPathSegments();
219        if (paths.size() < 4) {
220            throw new IllegalArgumentException("Not a document: " + documentUri);
221        }
222        if (!PATH_ROOTS.equals(paths.get(0))) {
223            throw new IllegalArgumentException("Not a document: " + documentUri);
224        }
225        if (!PATH_DOCS.equals(paths.get(2))) {
226            throw new IllegalArgumentException("Not a document: " + documentUri);
227        }
228        return paths.get(3);
229    }
230
231    public static String getSearchQuery(Uri documentUri) {
232        return documentUri.getQueryParameter(PARAM_QUERY);
233    }
234
235    /**
236     * These are standard columns for document URIs. Storage backend providers
237     * <em>must</em> support at least these columns when queried.
238     *
239     * @see Intent#ACTION_OPEN_DOCUMENT
240     * @see Intent#ACTION_CREATE_DOCUMENT
241     */
242    public interface DocumentColumns extends OpenableColumns {
243        /**
244         * The ID for a document under a storage backend root. Values
245         * <em>must</em> never change once returned. This field is read-only to
246         * document clients.
247         * <p>
248         * Type: STRING
249         */
250        public static final String DOC_ID = "doc_id";
251
252        /**
253         * MIME type of a document, matching the value returned by
254         * {@link ContentResolver#getType(android.net.Uri)}. This field must be
255         * provided when a new document is created, but after that the field is
256         * read-only.
257         * <p>
258         * Type: STRING
259         *
260         * @see DocumentsContract#MIME_TYPE_DIRECTORY
261         */
262        public static final String MIME_TYPE = "mime_type";
263
264        /**
265         * Timestamp when a document was last modified, in milliseconds since
266         * January 1, 1970 00:00:00.0 UTC. This field is read-only to document
267         * clients.
268         * <p>
269         * Type: INTEGER (long)
270         *
271         * @see System#currentTimeMillis()
272         */
273        public static final String LAST_MODIFIED = "last_modified";
274
275        /**
276         * Flags that apply to a specific document. This field is read-only to
277         * document clients.
278         * <p>
279         * Type: INTEGER (int)
280         */
281        public static final String FLAGS = "flags";
282
283        /**
284         * Summary for this document, or {@code null} to omit.
285         * <p>
286         * Type: STRING
287         */
288        public static final String SUMMARY = "summary";
289    }
290
291    /**
292     * Root that represents a cloud-based storage service.
293     *
294     * @see RootColumns#ROOT_TYPE
295     */
296    public static final int ROOT_TYPE_SERVICE = 1;
297
298    /**
299     * Root that represents a shortcut to content that may be available
300     * elsewhere through another storage root.
301     *
302     * @see RootColumns#ROOT_TYPE
303     */
304    public static final int ROOT_TYPE_SHORTCUT = 2;
305
306    /**
307     * Root that represents a physical storage device.
308     *
309     * @see RootColumns#ROOT_TYPE
310     */
311    public static final int ROOT_TYPE_DEVICE = 3;
312
313    /**
314     * Root that represents a physical storage device that should only be
315     * displayed to advanced users.
316     *
317     * @see RootColumns#ROOT_TYPE
318     */
319    public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
320
321    /**
322     * These are standard columns for the roots URI.
323     *
324     * @see DocumentsContract#buildRootsUri(String)
325     */
326    public interface RootColumns {
327        public static final String ROOT_ID = "root_id";
328
329        /**
330         * Storage root type, use for clustering.
331         * <p>
332         * Type: INTEGER (int)
333         *
334         * @see DocumentsContract#ROOT_TYPE_SERVICE
335         * @see DocumentsContract#ROOT_TYPE_DEVICE
336         */
337        public static final String ROOT_TYPE = "root_type";
338
339        /**
340         * Icon resource ID for this storage root, or {@code 0} to use the
341         * default {@link ProviderInfo#icon}.
342         * <p>
343         * Type: INTEGER (int)
344         */
345        public static final String ICON = "icon";
346
347        /**
348         * Title for this storage root, or {@code null} to use the default
349         * {@link ProviderInfo#labelRes}.
350         * <p>
351         * Type: STRING
352         */
353        public static final String TITLE = "title";
354
355        /**
356         * Summary for this storage root, or {@code null} to omit.
357         * <p>
358         * Type: STRING
359         */
360        public static final String SUMMARY = "summary";
361
362        /**
363         * Number of free bytes of available in this storage root, or -1 if
364         * unknown or unbounded.
365         * <p>
366         * Type: INTEGER (long)
367         */
368        public static final String AVAILABLE_BYTES = "available_bytes";
369    }
370
371    /**
372     * Return list of all documents that the calling package has "open." These
373     * are Uris matching {@link DocumentsContract} to which persistent
374     * read/write access has been granted, usually through
375     * {@link Intent#ACTION_OPEN_DOCUMENT} or
376     * {@link Intent#ACTION_CREATE_DOCUMENT}.
377     *
378     * @see Context#grantUriPermission(String, Uri, int)
379     * @see ContentResolver#getIncomingUriPermissionGrants(int, int)
380     */
381    public static Uri[] getOpenDocuments(Context context) {
382        final int openedFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
383                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION;
384        final Uri[] uris = context.getContentResolver()
385                .getIncomingUriPermissionGrants(openedFlags, openedFlags);
386
387        // Filter to only include document providers
388        final PackageManager pm = context.getPackageManager();
389        final List<Uri> result = Lists.newArrayList();
390        for (Uri uri : uris) {
391            final ProviderInfo info = pm.resolveContentProvider(
392                    uri.getAuthority(), PackageManager.GET_META_DATA);
393            if (info.metaData.containsKey(META_DATA_DOCUMENT_PROVIDER)) {
394                result.add(uri);
395            }
396        }
397
398        return result.toArray(new Uri[result.size()]);
399    }
400
401    /**
402     * Return thumbnail representing the document at the given URI. Callers are
403     * responsible for their own caching. Given document must have
404     * {@link #FLAG_SUPPORTS_THUMBNAIL} set.
405     *
406     * @return decoded thumbnail, or {@code null} if problem was encountered.
407     */
408    public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
409        final Bundle opts = new Bundle();
410        opts.putParcelable(EXTRA_THUMBNAIL_SIZE, size);
411
412        InputStream is = null;
413        try {
414            is = new AssetFileDescriptor.AutoCloseInputStream(
415                    resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts));
416            return BitmapFactory.decodeStream(is);
417        } catch (IOException e) {
418            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
419            return null;
420        } finally {
421            IoUtils.closeQuietly(is);
422        }
423    }
424
425    /**
426     * Rename the document at the given URI. Given document must have
427     * {@link #FLAG_SUPPORTS_RENAME} set.
428     *
429     * @return if rename was successful.
430     */
431    public static boolean renameDocument(
432            ContentResolver resolver, Uri documentUri, String displayName) {
433        final ContentValues values = new ContentValues();
434        values.put(DocumentColumns.DISPLAY_NAME, displayName);
435        return (resolver.update(documentUri, values, null, null) == 1);
436    }
437}
438