DocumentsContract.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.net.TrafficStats.KB_IN_BYTES; 20import static libcore.io.OsConstants.SEEK_SET; 21 22import android.content.ContentProviderClient; 23import android.content.ContentResolver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.pm.PackageManager; 27import android.content.pm.ProviderInfo; 28import android.content.res.AssetFileDescriptor; 29import android.database.Cursor; 30import android.graphics.Bitmap; 31import android.graphics.BitmapFactory; 32import android.graphics.Point; 33import android.graphics.drawable.Drawable; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.Parcel; 37import android.os.ParcelFileDescriptor; 38import android.os.ParcelFileDescriptor.OnCloseListener; 39import android.os.Parcelable; 40import android.util.Log; 41 42import com.android.internal.util.Preconditions; 43import com.google.android.collect.Lists; 44 45import libcore.io.ErrnoException; 46import libcore.io.IoBridge; 47import libcore.io.IoUtils; 48import libcore.io.Libcore; 49 50import java.io.FileDescriptor; 51import java.io.IOException; 52import java.util.List; 53 54/** 55 * Defines the contract between a documents provider and the platform. 56 * <p> 57 * To create a document provider, extend {@link DocumentsProvider}, which 58 * provides a foundational implementation of this contract. 59 * 60 * @see DocumentsProvider 61 */ 62public final class DocumentsContract { 63 private static final String TAG = "Documents"; 64 65 // content://com.example/docs/12/ 66 // content://com.example/docs/12/children/ 67 // content://com.example/docs/12/search/?query=pony 68 69 private DocumentsContract() { 70 } 71 72 /** {@hide} */ 73 public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER"; 74 75 /** {@hide} */ 76 public static final String ACTION_MANAGE_DOCUMENTS = "android.provider.action.MANAGE_DOCUMENTS"; 77 78 /** {@hide} */ 79 public static final String 80 ACTION_DOCUMENT_ROOT_CHANGED = "android.provider.action.DOCUMENT_ROOT_CHANGED"; 81 82 /** 83 * Constants for individual documents. 84 */ 85 public final static class Documents { 86 private Documents() { 87 } 88 89 /** 90 * MIME type of a document which is a directory that may contain additional 91 * documents. 92 */ 93 public static final String MIME_TYPE_DIR = "vnd.android.doc/dir"; 94 95 /** 96 * Flag indicating that a document is a directory that supports creation of 97 * new files within it. 98 * 99 * @see DocumentColumns#FLAGS 100 */ 101 public static final int FLAG_SUPPORTS_CREATE = 1; 102 103 /** 104 * Flag indicating that a document is renamable. 105 * 106 * @see DocumentColumns#FLAGS 107 */ 108 public static final int FLAG_SUPPORTS_RENAME = 1 << 1; 109 110 /** 111 * Flag indicating that a document is deletable. 112 * 113 * @see DocumentColumns#FLAGS 114 */ 115 public static final int FLAG_SUPPORTS_DELETE = 1 << 2; 116 117 /** 118 * Flag indicating that a document can be represented as a thumbnail. 119 * 120 * @see DocumentColumns#FLAGS 121 */ 122 public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3; 123 124 /** 125 * Flag indicating that a document is a directory that supports search. 126 * 127 * @see DocumentColumns#FLAGS 128 */ 129 public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; 130 131 /** 132 * Flag indicating that a document supports writing. 133 * 134 * @see DocumentColumns#FLAGS 135 */ 136 public static final int FLAG_SUPPORTS_WRITE = 1 << 5; 137 138 /** 139 * Flag indicating that a document is a directory that prefers its contents 140 * be shown in a larger format grid. Usually suitable when a directory 141 * contains mostly pictures. 142 * 143 * @see DocumentColumns#FLAGS 144 */ 145 public static final int FLAG_PREFERS_GRID = 1 << 6; 146 } 147 148 /** 149 * Extra boolean flag included in a directory {@link Cursor#getExtras()} 150 * indicating that a document provider is still loading data. For example, a 151 * provider has returned some results, but is still waiting on an 152 * outstanding network request. 153 * 154 * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, 155 * boolean) 156 */ 157 public static final String EXTRA_LOADING = "loading"; 158 159 /** 160 * Extra string included in a directory {@link Cursor#getExtras()} 161 * providing an informational message that should be shown to a user. For 162 * example, a provider may wish to indicate that not all documents are 163 * available. 164 */ 165 public static final String EXTRA_INFO = "info"; 166 167 /** 168 * Extra string included in a directory {@link Cursor#getExtras()} providing 169 * an error message that should be shown to a user. For example, a provider 170 * may wish to indicate that a network error occurred. The user may choose 171 * to retry, resulting in a new query. 172 */ 173 public static final String EXTRA_ERROR = "error"; 174 175 /** {@hide} */ 176 public static final String METHOD_GET_ROOTS = "android:getRoots"; 177 /** {@hide} */ 178 public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; 179 /** {@hide} */ 180 public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument"; 181 /** {@hide} */ 182 public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; 183 184 /** {@hide} */ 185 public static final String EXTRA_AUTHORITY = "authority"; 186 /** {@hide} */ 187 public static final String EXTRA_PACKAGE_NAME = "packageName"; 188 /** {@hide} */ 189 public static final String EXTRA_URI = "uri"; 190 /** {@hide} */ 191 public static final String EXTRA_ROOTS = "roots"; 192 /** {@hide} */ 193 public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; 194 195 private static final String PATH_DOCS = "docs"; 196 private static final String PATH_CHILDREN = "children"; 197 private static final String PATH_SEARCH = "search"; 198 199 private static final String PARAM_QUERY = "query"; 200 201 /** 202 * Build Uri representing the given {@link DocumentColumns#DOC_ID} in a 203 * document provider. 204 */ 205 public static Uri buildDocumentUri(String authority, String docId) { 206 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 207 .authority(authority).appendPath(PATH_DOCS).appendPath(docId).build(); 208 } 209 210 /** 211 * Build Uri representing the contents of the given directory in a document 212 * provider. The given document must be {@link Documents#MIME_TYPE_DIR}. 213 * 214 * @hide 215 */ 216 public static Uri buildChildrenUri(String authority, String docId) { 217 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) 218 .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_CHILDREN).build(); 219 } 220 221 /** 222 * Build Uri representing a search for matching documents under a specific 223 * directory in a document provider. The given document must have 224 * {@link Documents#FLAG_SUPPORTS_SEARCH}. 225 * 226 * @hide 227 */ 228 public static Uri buildSearchUri(String authority, String docId, String query) { 229 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) 230 .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_SEARCH) 231 .appendQueryParameter(PARAM_QUERY, query).build(); 232 } 233 234 /** 235 * Extract the {@link DocumentColumns#DOC_ID} from the given Uri. 236 */ 237 public static String getDocId(Uri documentUri) { 238 final List<String> paths = documentUri.getPathSegments(); 239 if (paths.size() < 2) { 240 throw new IllegalArgumentException("Not a document: " + documentUri); 241 } 242 if (!PATH_DOCS.equals(paths.get(0))) { 243 throw new IllegalArgumentException("Not a document: " + documentUri); 244 } 245 return paths.get(1); 246 } 247 248 /** {@hide} */ 249 public static String getSearchQuery(Uri documentUri) { 250 return documentUri.getQueryParameter(PARAM_QUERY); 251 } 252 253 /** 254 * Standard columns for document queries. Document providers <em>must</em> 255 * support at least these columns when queried. 256 */ 257 public interface DocumentColumns extends OpenableColumns { 258 /** 259 * Unique ID for a document. Values <em>must</em> never change once 260 * returned, since they may used for long-term Uri permission grants. 261 * <p> 262 * Type: STRING 263 */ 264 public static final String DOC_ID = "doc_id"; 265 266 /** 267 * MIME type of a document. 268 * <p> 269 * Type: STRING 270 * 271 * @see Documents#MIME_TYPE_DIR 272 */ 273 public static final String MIME_TYPE = "mime_type"; 274 275 /** 276 * Timestamp when a document was last modified, in milliseconds since 277 * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. Document 278 * providers can update this field using events from 279 * {@link OnCloseListener} or other reliable 280 * {@link ParcelFileDescriptor} transports. 281 * <p> 282 * Type: INTEGER (long) 283 * 284 * @see System#currentTimeMillis() 285 */ 286 public static final String LAST_MODIFIED = "last_modified"; 287 288 /** 289 * Specific icon resource for a document, or {@code null} to resolve 290 * default using {@link #MIME_TYPE}. 291 * <p> 292 * Type: INTEGER (int) 293 */ 294 public static final String ICON = "icon"; 295 296 /** 297 * Summary for a document, or {@code null} to omit. 298 * <p> 299 * Type: STRING 300 */ 301 public static final String SUMMARY = "summary"; 302 303 /** 304 * Flags that apply to a specific document. 305 * <p> 306 * Type: INTEGER (int) 307 */ 308 public static final String FLAGS = "flags"; 309 } 310 311 /** 312 * Metadata about a specific root of documents. 313 */ 314 public final static class DocumentRoot implements Parcelable { 315 /** 316 * Root that represents a storage service, such as a cloud-based 317 * service. 318 * 319 * @see #rootType 320 */ 321 public static final int ROOT_TYPE_SERVICE = 1; 322 323 /** 324 * Root that represents a shortcut to content that may be available 325 * elsewhere through another storage root. 326 * 327 * @see #rootType 328 */ 329 public static final int ROOT_TYPE_SHORTCUT = 2; 330 331 /** 332 * Root that represents a physical storage device. 333 * 334 * @see #rootType 335 */ 336 public static final int ROOT_TYPE_DEVICE = 3; 337 338 /** 339 * Root that represents a physical storage device that should only be 340 * displayed to advanced users. 341 * 342 * @see #rootType 343 */ 344 public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; 345 346 /** 347 * Flag indicating that at least one directory under this root supports 348 * creating content. 349 * 350 * @see #flags 351 */ 352 public static final int FLAG_SUPPORTS_CREATE = 1; 353 354 /** 355 * Flag indicating that this root offers content that is strictly local 356 * on the device. That is, no network requests are made for the content. 357 * 358 * @see #flags 359 */ 360 public static final int FLAG_LOCAL_ONLY = 1 << 1; 361 362 /** {@hide} */ 363 public String authority; 364 365 /** 366 * Root type, use for clustering. 367 * 368 * @see #ROOT_TYPE_SERVICE 369 * @see #ROOT_TYPE_DEVICE 370 */ 371 public int rootType; 372 373 /** 374 * Flags for this root. 375 * 376 * @see #FLAG_LOCAL_ONLY 377 */ 378 public int flags; 379 380 /** 381 * Icon resource ID for this root. 382 */ 383 public int icon; 384 385 /** 386 * Title for this root. 387 */ 388 public String title; 389 390 /** 391 * Summary for this root. May be {@code null}. 392 */ 393 public String summary; 394 395 /** 396 * Document which is a directory that represents the top of this root. 397 * Must not be {@code null}. 398 * 399 * @see DocumentColumns#DOC_ID 400 */ 401 public String docId; 402 403 /** 404 * Document which is a directory representing recently modified 405 * documents under this root. This directory should return at most two 406 * dozen documents modified within the last 90 days. May be {@code null} 407 * if this root doesn't support recents. 408 * 409 * @see DocumentColumns#DOC_ID 410 */ 411 public String recentDocId; 412 413 /** 414 * Number of free bytes of available in this root, or -1 if unknown or 415 * unbounded. 416 */ 417 public long availableBytes; 418 419 /** 420 * Set of MIME type filters describing the content offered by this root, 421 * or {@code null} to indicate that all MIME types are supported. For 422 * example, a provider only supporting audio and video might set this to 423 * {@code ["audio/*", "video/*"]}. 424 */ 425 public String[] mimeTypes; 426 427 public DocumentRoot() { 428 } 429 430 /** {@hide} */ 431 public DocumentRoot(Parcel in) { 432 rootType = in.readInt(); 433 flags = in.readInt(); 434 icon = in.readInt(); 435 title = in.readString(); 436 summary = in.readString(); 437 docId = in.readString(); 438 recentDocId = in.readString(); 439 availableBytes = in.readLong(); 440 mimeTypes = in.readStringArray(); 441 } 442 443 /** {@hide} */ 444 public Drawable loadIcon(Context context) { 445 if (icon != 0) { 446 if (authority != null) { 447 final PackageManager pm = context.getPackageManager(); 448 final ProviderInfo info = pm.resolveContentProvider(authority, 0); 449 if (info != null) { 450 return pm.getDrawable(info.packageName, icon, info.applicationInfo); 451 } 452 } else { 453 return context.getResources().getDrawable(icon); 454 } 455 } 456 return null; 457 } 458 459 @Override 460 public int describeContents() { 461 return 0; 462 } 463 464 @Override 465 public void writeToParcel(Parcel dest, int flags) { 466 Preconditions.checkNotNull(docId); 467 468 dest.writeInt(rootType); 469 dest.writeInt(flags); 470 dest.writeInt(icon); 471 dest.writeString(title); 472 dest.writeString(summary); 473 dest.writeString(docId); 474 dest.writeString(recentDocId); 475 dest.writeLong(availableBytes); 476 dest.writeStringArray(mimeTypes); 477 } 478 479 public static final Creator<DocumentRoot> CREATOR = new Creator<DocumentRoot>() { 480 @Override 481 public DocumentRoot createFromParcel(Parcel in) { 482 return new DocumentRoot(in); 483 } 484 485 @Override 486 public DocumentRoot[] newArray(int size) { 487 return new DocumentRoot[size]; 488 } 489 }; 490 } 491 492 /** 493 * Return list of all documents that the calling package has "open." These 494 * are Uris matching {@link DocumentsContract} to which persistent 495 * read/write access has been granted, usually through 496 * {@link Intent#ACTION_OPEN_DOCUMENT} or 497 * {@link Intent#ACTION_CREATE_DOCUMENT}. 498 * 499 * @see Context#grantUriPermission(String, Uri, int) 500 * @see ContentResolver#getIncomingUriPermissionGrants(int, int) 501 */ 502 public static Uri[] getOpenDocuments(Context context) { 503 final int openedFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION 504 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION; 505 final Uri[] uris = context.getContentResolver() 506 .getIncomingUriPermissionGrants(openedFlags, openedFlags); 507 508 // Filter to only include document providers 509 final PackageManager pm = context.getPackageManager(); 510 final List<Uri> result = Lists.newArrayList(); 511 for (Uri uri : uris) { 512 final ProviderInfo info = pm.resolveContentProvider( 513 uri.getAuthority(), PackageManager.GET_META_DATA); 514 if (info.metaData.containsKey(META_DATA_DOCUMENT_PROVIDER)) { 515 result.add(uri); 516 } 517 } 518 519 return result.toArray(new Uri[result.size()]); 520 } 521 522 /** 523 * Return thumbnail representing the document at the given URI. Callers are 524 * responsible for their own in-memory caching. Given document must have 525 * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set. 526 * 527 * @return decoded thumbnail, or {@code null} if problem was encountered. 528 * @hide 529 */ 530 public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) { 531 final Bundle openOpts = new Bundle(); 532 openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size); 533 534 AssetFileDescriptor afd = null; 535 try { 536 afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts); 537 538 final FileDescriptor fd = afd.getFileDescriptor(); 539 final long offset = afd.getStartOffset(); 540 final long length = afd.getDeclaredLength(); 541 542 // Some thumbnails might be a region inside a larger file, such as 543 // an EXIF thumbnail. Since BitmapFactory aggressively seeks around 544 // the entire file, we read the region manually. 545 byte[] region = null; 546 if (offset > 0 && length <= 64 * KB_IN_BYTES) { 547 region = new byte[(int) length]; 548 Libcore.os.lseek(fd, offset, SEEK_SET); 549 if (IoBridge.read(fd, region, 0, region.length) != region.length) { 550 region = null; 551 } 552 } 553 554 // We requested a rough thumbnail size, but the remote size may have 555 // returned something giant, so defensively scale down as needed. 556 final BitmapFactory.Options opts = new BitmapFactory.Options(); 557 opts.inJustDecodeBounds = true; 558 if (region != null) { 559 BitmapFactory.decodeByteArray(region, 0, region.length, opts); 560 } else { 561 BitmapFactory.decodeFileDescriptor(fd, null, opts); 562 } 563 564 final int widthSample = opts.outWidth / size.x; 565 final int heightSample = opts.outHeight / size.y; 566 567 opts.inJustDecodeBounds = false; 568 opts.inSampleSize = Math.min(widthSample, heightSample); 569 Log.d(TAG, "Decoding with sample size " + opts.inSampleSize); 570 if (region != null) { 571 return BitmapFactory.decodeByteArray(region, 0, region.length, opts); 572 } else { 573 return BitmapFactory.decodeFileDescriptor(fd, null, opts); 574 } 575 } catch (ErrnoException e) { 576 Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e); 577 return null; 578 } catch (IOException e) { 579 Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e); 580 return null; 581 } finally { 582 IoUtils.closeQuietly(afd); 583 } 584 } 585 586 /** {@hide} */ 587 public static List<DocumentRoot> getDocumentRoots(ContentProviderClient client) { 588 try { 589 final Bundle out = client.call(METHOD_GET_ROOTS, null, null); 590 final List<DocumentRoot> roots = out.getParcelableArrayList(EXTRA_ROOTS); 591 return roots; 592 } catch (Exception e) { 593 Log.w(TAG, "Failed to get roots", e); 594 return null; 595 } 596 } 597 598 /** 599 * Create a new document under the given parent document with MIME type and 600 * display name. 601 * 602 * @param docId document with {@link Documents#FLAG_SUPPORTS_CREATE} 603 * @param mimeType MIME type of new document 604 * @param displayName name of new document 605 * @return newly created document, or {@code null} if failed 606 * @hide 607 */ 608 public static String createDocument( 609 ContentProviderClient client, String docId, String mimeType, String displayName) { 610 final Bundle in = new Bundle(); 611 in.putString(DocumentColumns.DOC_ID, docId); 612 in.putString(DocumentColumns.MIME_TYPE, mimeType); 613 in.putString(DocumentColumns.DISPLAY_NAME, displayName); 614 615 try { 616 final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in); 617 return out.getString(DocumentColumns.DOC_ID); 618 } catch (Exception e) { 619 Log.w(TAG, "Failed to create document", e); 620 return null; 621 } 622 } 623 624 /** 625 * Rename the given document. 626 * 627 * @param docId document with {@link Documents#FLAG_SUPPORTS_RENAME} 628 * @return document which may have changed due to rename, or {@code null} if 629 * rename failed. 630 * @hide 631 */ 632 public static String renameDocument( 633 ContentProviderClient client, String docId, String displayName) { 634 final Bundle in = new Bundle(); 635 in.putString(DocumentColumns.DOC_ID, docId); 636 in.putString(DocumentColumns.DISPLAY_NAME, displayName); 637 638 try { 639 final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in); 640 return out.getString(DocumentColumns.DOC_ID); 641 } catch (Exception e) { 642 Log.w(TAG, "Failed to rename document", e); 643 return null; 644 } 645 } 646 647 /** 648 * Delete the given document. 649 * 650 * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE} 651 * @hide 652 */ 653 public static boolean deleteDocument(ContentProviderClient client, String docId) { 654 final Bundle in = new Bundle(); 655 in.putString(DocumentColumns.DOC_ID, docId); 656 657 try { 658 client.call(METHOD_DELETE_DOCUMENT, null, in); 659 return true; 660 } catch (Exception e) { 661 Log.w(TAG, "Failed to delete document", e); 662 return false; 663 } 664 } 665} 666