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