DocumentsProvider.java revision 37ed78e504ef3666dd5fce15ff4994f151c44fcd
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.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE; 20import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; 21import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; 22import static android.provider.DocumentsContract.getDocumentId; 23import static android.provider.DocumentsContract.getRootId; 24import static android.provider.DocumentsContract.getSearchDocumentsQuery; 25 26import android.content.ContentProvider; 27import android.content.ContentResolver; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.UriMatcher; 32import android.content.pm.PackageManager; 33import android.content.pm.ProviderInfo; 34import android.content.res.AssetFileDescriptor; 35import android.database.Cursor; 36import android.graphics.Point; 37import android.net.Uri; 38import android.os.Bundle; 39import android.os.CancellationSignal; 40import android.os.ParcelFileDescriptor; 41import android.os.ParcelFileDescriptor.OnCloseListener; 42import android.provider.DocumentsContract.Document; 43import android.provider.DocumentsContract.Root; 44import android.util.Log; 45 46import libcore.io.IoUtils; 47 48import java.io.FileNotFoundException; 49 50/** 51 * Base class for a document provider. A document provider offers read and write 52 * access to durable files, such as files stored on a local disk, or files in a 53 * cloud storage service. To create a document provider, extend this class, 54 * implement the abstract methods, and add it to your manifest like this: 55 * 56 * <pre class="prettyprint"><manifest> 57 * ... 58 * <application> 59 * ... 60 * <provider 61 * android:name="com.example.MyCloudProvider" 62 * android:authorities="com.example.mycloudprovider" 63 * android:exported="true" 64 * android:grantUriPermissions="true" 65 * android:permission="android.permission.MANAGE_DOCUMENTS"> 66 * <intent-filter> 67 * <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> 68 * </intent-filter> 69 * </provider> 70 * ... 71 * </application> 72 *</manifest></pre> 73 * <p> 74 * When defining your provider, you must protect it with 75 * {@link android.Manifest.permission#MANAGE_DOCUMENTS}, which is a permission 76 * only the system can obtain. Applications cannot use a documents provider 77 * directly; they must go through {@link Intent#ACTION_OPEN_DOCUMENT} or 78 * {@link Intent#ACTION_CREATE_DOCUMENT} which requires a user to actively 79 * navigate and select documents. When a user selects documents through that UI, 80 * the system issues narrow URI permission grants to the requesting application. 81 * </p> 82 * <h3>Documents</h3> 83 * <p> 84 * A document can be either an openable stream (with a specific MIME type), or a 85 * directory containing additional documents (with the 86 * {@link Document#MIME_TYPE_DIR} MIME type). Each directory represents the top 87 * of a subtree containing zero or more documents, which can recursively contain 88 * even more documents and directories. 89 * </p> 90 * <p> 91 * Each document can have different capabilities, as described by 92 * {@link Document#COLUMN_FLAGS}. For example, if a document can be represented 93 * as a thumbnail, your provider can set 94 * {@link Document#FLAG_SUPPORTS_THUMBNAIL} and implement 95 * {@link #openDocumentThumbnail(String, Point, CancellationSignal)} to return 96 * that thumbnail. 97 * </p> 98 * <p> 99 * Each document under a provider is uniquely referenced by its 100 * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. A 101 * single document can be included in multiple directories when responding to 102 * {@link #queryChildDocuments(String, String[], String)}. For example, a 103 * provider might surface a single photo in multiple locations: once in a 104 * directory of geographic locations, and again in a directory of dates. 105 * </p> 106 * <h3>Roots</h3> 107 * <p> 108 * All documents are surfaced through one or more "roots." Each root represents 109 * the top of a document tree that a user can navigate. For example, a root 110 * could represent an account or a physical storage device. Similar to 111 * documents, each root can have capabilities expressed through 112 * {@link Root#COLUMN_FLAGS}. 113 * </p> 114 * 115 * @see Intent#ACTION_OPEN_DOCUMENT 116 * @see Intent#ACTION_CREATE_DOCUMENT 117 */ 118public abstract class DocumentsProvider extends ContentProvider { 119 private static final String TAG = "DocumentsProvider"; 120 121 private static final int MATCH_ROOTS = 1; 122 private static final int MATCH_ROOT = 2; 123 private static final int MATCH_RECENT = 3; 124 private static final int MATCH_SEARCH = 4; 125 private static final int MATCH_DOCUMENT = 5; 126 private static final int MATCH_CHILDREN = 6; 127 128 private String mAuthority; 129 130 private UriMatcher mMatcher; 131 132 /** 133 * Implementation is provided by the parent class. 134 */ 135 @Override 136 public void attachInfo(Context context, ProviderInfo info) { 137 mAuthority = info.authority; 138 139 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 140 mMatcher.addURI(mAuthority, "root", MATCH_ROOTS); 141 mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT); 142 mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); 143 mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); 144 mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); 145 mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); 146 147 // Sanity check our setup 148 if (!info.exported) { 149 throw new SecurityException("Provider must be exported"); 150 } 151 if (!info.grantUriPermissions) { 152 throw new SecurityException("Provider must grantUriPermissions"); 153 } 154 if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission) 155 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) { 156 throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS"); 157 } 158 159 super.attachInfo(context, info); 160 } 161 162 /** 163 * Create a new document and return its newly generated 164 * {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new 165 * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must 166 * not change once returned. 167 * 168 * @param parentDocumentId the parent directory to create the new document 169 * under. 170 * @param mimeType the concrete MIME type associated with the new document. 171 * If the MIME type is not supported, the provider must throw. 172 * @param displayName the display name of the new document. The provider may 173 * alter this name to meet any internal constraints, such as 174 * conflicting names. 175 */ 176 @SuppressWarnings("unused") 177 public String createDocument(String parentDocumentId, String mimeType, String displayName) 178 throws FileNotFoundException { 179 throw new UnsupportedOperationException("Create not supported"); 180 } 181 182 /** 183 * Delete the requested document. Upon returning, any URI permission grants 184 * for the requested document will be revoked. If additional documents were 185 * deleted as a side effect of this call, such as documents inside a 186 * directory, the implementor is responsible for revoking those permissions. 187 * 188 * @param documentId the document to delete. 189 */ 190 @SuppressWarnings("unused") 191 public void deleteDocument(String documentId) throws FileNotFoundException { 192 throw new UnsupportedOperationException("Delete not supported"); 193 } 194 195 /** 196 * Return all roots currently provided. To display to users, you must define 197 * at least one root. You should avoid making network requests to keep this 198 * request fast. 199 * <p> 200 * Each root is defined by the metadata columns described in {@link Root}, 201 * including {@link Root#COLUMN_DOCUMENT_ID} which points to a directory 202 * representing a tree of documents to display under that root. 203 * <p> 204 * If this set of roots changes, you must call {@link ContentResolver#notifyChange(Uri, 205 * android.database.ContentObserver, boolean)} with 206 * {@link DocumentsContract#buildRootsUri(String)} to notify the system. 207 * 208 * @param projection list of {@link Root} columns to put into the cursor. If 209 * {@code null} all supported columns should be included. 210 */ 211 public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException; 212 213 /** 214 * Return recently modified documents under the requested root. This will 215 * only be called for roots that advertise 216 * {@link Root#FLAG_SUPPORTS_RECENTS}. The returned documents should be 217 * sorted by {@link Document#COLUMN_LAST_MODIFIED} in descending order, and 218 * limited to only return the 64 most recently modified documents. 219 * <p> 220 * Recent documents do not support change notifications. 221 * 222 * @param projection list of {@link Document} columns to put into the 223 * cursor. If {@code null} all supported columns should be 224 * included. 225 * @see DocumentsContract#EXTRA_LOADING 226 */ 227 @SuppressWarnings("unused") 228 public Cursor queryRecentDocuments(String rootId, String[] projection) 229 throws FileNotFoundException { 230 throw new UnsupportedOperationException("Recent not supported"); 231 } 232 233 /** 234 * Return metadata for the single requested document. You should avoid 235 * making network requests to keep this request fast. 236 * 237 * @param documentId the document to return. 238 * @param projection list of {@link Document} columns to put into the 239 * cursor. If {@code null} all supported columns should be 240 * included. 241 */ 242 public abstract Cursor queryDocument(String documentId, String[] projection) 243 throws FileNotFoundException; 244 245 /** 246 * Return the children documents contained in the requested directory. This 247 * must only return immediate descendants, as additional queries will be 248 * issued to recursively explore the tree. 249 * <p> 250 * If your provider is cloud-based, and you have some data cached or pinned 251 * locally, you may return the local data immediately, setting 252 * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that 253 * you are still fetching additional data. Then, when the network data is 254 * available, you can send a change notification to trigger a requery and 255 * return the complete contents. 256 * <p> 257 * To support change notifications, you must 258 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 259 * Uri, such as 260 * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then 261 * you can call {@link ContentResolver#notifyChange(Uri, 262 * android.database.ContentObserver, boolean)} with that Uri to send change 263 * notifications. 264 * 265 * @param parentDocumentId the directory to return children for. 266 * @param projection list of {@link Document} columns to put into the 267 * cursor. If {@code null} all supported columns should be 268 * included. 269 * @param sortOrder how to order the rows, formatted as an SQL 270 * {@code ORDER BY} clause (excluding the ORDER BY itself). 271 * Passing {@code null} will use the default sort order, which 272 * may be unordered. This ordering is a hint that can be used to 273 * prioritize how data is fetched from the network, but UI may 274 * always enforce a specific ordering. 275 * @see DocumentsContract#EXTRA_LOADING 276 * @see DocumentsContract#EXTRA_INFO 277 * @see DocumentsContract#EXTRA_ERROR 278 */ 279 public abstract Cursor queryChildDocuments( 280 String parentDocumentId, String[] projection, String sortOrder) 281 throws FileNotFoundException; 282 283 /** {@hide} */ 284 @SuppressWarnings("unused") 285 public Cursor queryChildDocumentsForManage( 286 String parentDocumentId, String[] projection, String sortOrder) 287 throws FileNotFoundException { 288 throw new UnsupportedOperationException("Manage not supported"); 289 } 290 291 /** 292 * Return documents that that match the given query under the requested 293 * root. The returned documents should be sorted by relevance in descending 294 * order. How documents are matched against the query string is an 295 * implementation detail left to each provider, but it's suggested that at 296 * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a 297 * case-insensitive fashion. 298 * <p> 299 * Only documents may be returned; directories are not supported in search 300 * results. 301 * <p> 302 * If your provider is cloud-based, and you have some data cached or pinned 303 * locally, you may return the local data immediately, setting 304 * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that 305 * you are still fetching additional data. Then, when the network data is 306 * available, you can send a change notification to trigger a requery and 307 * return the complete contents. 308 * <p> 309 * To support change notifications, you must 310 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 311 * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String, 312 * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri, 313 * android.database.ContentObserver, boolean)} with that Uri to send change 314 * notifications. 315 * 316 * @param rootId the root to search under. 317 * @param query string to match documents against. 318 * @param projection list of {@link Document} columns to put into the 319 * cursor. If {@code null} all supported columns should be 320 * included. 321 * @see DocumentsContract#EXTRA_LOADING 322 * @see DocumentsContract#EXTRA_INFO 323 * @see DocumentsContract#EXTRA_ERROR 324 */ 325 @SuppressWarnings("unused") 326 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 327 throws FileNotFoundException { 328 throw new UnsupportedOperationException("Search not supported"); 329 } 330 331 /** 332 * Return concrete MIME type of the requested document. Must match the value 333 * of {@link Document#COLUMN_MIME_TYPE} for this document. The default 334 * implementation queries {@link #queryDocument(String, String[])}, so 335 * providers may choose to override this as an optimization. 336 */ 337 public String getDocumentType(String documentId) throws FileNotFoundException { 338 final Cursor cursor = queryDocument(documentId, null); 339 try { 340 if (cursor.moveToFirst()) { 341 return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); 342 } else { 343 return null; 344 } 345 } finally { 346 IoUtils.closeQuietly(cursor); 347 } 348 } 349 350 /** 351 * Open and return the requested document. 352 * <p> 353 * Your provider should return a reliable {@link ParcelFileDescriptor} to 354 * detect when the remote caller has finished reading or writing the 355 * document. You may return a pipe or socket pair if the mode is exclusively 356 * "r" or "w", but complex modes like "rw" imply a normal file on disk that 357 * supports seeking. 358 * <p> 359 * If you block while downloading content, you should periodically check 360 * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. 361 * 362 * @param documentId the document to return. 363 * @param mode the mode to open with, such as 'r', 'w', or 'rw'. 364 * @param signal used by the caller to signal if the request should be 365 * cancelled. 366 * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, 367 * OnCloseListener) 368 * @see ParcelFileDescriptor#createReliablePipe() 369 * @see ParcelFileDescriptor#createReliableSocketPair() 370 * @see ParcelFileDescriptor#parseMode(String) 371 */ 372 public abstract ParcelFileDescriptor openDocument( 373 String documentId, String mode, CancellationSignal signal) throws FileNotFoundException; 374 375 /** 376 * Open and return a thumbnail of the requested document. 377 * <p> 378 * A provider should return a thumbnail closely matching the hinted size, 379 * attempting to serve from a local cache if possible. A provider should 380 * never return images more than double the hinted size. 381 * <p> 382 * If you perform expensive operations to download or generate a thumbnail, 383 * you should periodically check {@link CancellationSignal#isCanceled()} to 384 * abort abandoned thumbnail requests. 385 * 386 * @param documentId the document to return. 387 * @param sizeHint hint of the optimal thumbnail dimensions. 388 * @param signal used by the caller to signal if the request should be 389 * cancelled. 390 * @see Document#FLAG_SUPPORTS_THUMBNAIL 391 */ 392 @SuppressWarnings("unused") 393 public AssetFileDescriptor openDocumentThumbnail( 394 String documentId, Point sizeHint, CancellationSignal signal) 395 throws FileNotFoundException { 396 throw new UnsupportedOperationException("Thumbnails not supported"); 397 } 398 399 /** 400 * Implementation is provided by the parent class. Cannot be overriden. 401 * 402 * @see #queryRoots(String[]) 403 * @see #queryRecentDocuments(String, String[]) 404 * @see #queryDocument(String, String[]) 405 * @see #queryChildDocuments(String, String[], String) 406 * @see #querySearchDocuments(String, String, String[]) 407 */ 408 @Override 409 public final Cursor query(Uri uri, String[] projection, String selection, 410 String[] selectionArgs, String sortOrder) { 411 try { 412 switch (mMatcher.match(uri)) { 413 case MATCH_ROOTS: 414 return queryRoots(projection); 415 case MATCH_RECENT: 416 return queryRecentDocuments(getRootId(uri), projection); 417 case MATCH_SEARCH: 418 return querySearchDocuments( 419 getRootId(uri), getSearchDocumentsQuery(uri), projection); 420 case MATCH_DOCUMENT: 421 return queryDocument(getDocumentId(uri), projection); 422 case MATCH_CHILDREN: 423 if (DocumentsContract.isManageMode(uri)) { 424 return queryChildDocumentsForManage( 425 getDocumentId(uri), projection, sortOrder); 426 } else { 427 return queryChildDocuments(getDocumentId(uri), projection, sortOrder); 428 } 429 default: 430 throw new UnsupportedOperationException("Unsupported Uri " + uri); 431 } 432 } catch (FileNotFoundException e) { 433 Log.w(TAG, "Failed during query", e); 434 return null; 435 } 436 } 437 438 /** 439 * Implementation is provided by the parent class. Cannot be overriden. 440 * 441 * @see #getDocumentType(String) 442 */ 443 @Override 444 public final String getType(Uri uri) { 445 try { 446 switch (mMatcher.match(uri)) { 447 case MATCH_ROOT: 448 return DocumentsContract.Root.MIME_TYPE_ITEM; 449 case MATCH_DOCUMENT: 450 return getDocumentType(getDocumentId(uri)); 451 default: 452 return null; 453 } 454 } catch (FileNotFoundException e) { 455 Log.w(TAG, "Failed during getType", e); 456 return null; 457 } 458 } 459 460 /** 461 * Implementation is provided by the parent class. Throws by default, and 462 * cannot be overriden. 463 * 464 * @see #createDocument(String, String, String) 465 */ 466 @Override 467 public final Uri insert(Uri uri, ContentValues values) { 468 throw new UnsupportedOperationException("Insert not supported"); 469 } 470 471 /** 472 * Implementation is provided by the parent class. Throws by default, and 473 * cannot be overriden. 474 * 475 * @see #deleteDocument(String) 476 */ 477 @Override 478 public final int delete(Uri uri, String selection, String[] selectionArgs) { 479 throw new UnsupportedOperationException("Delete not supported"); 480 } 481 482 /** 483 * Implementation is provided by the parent class. Throws by default, and 484 * cannot be overriden. 485 */ 486 @Override 487 public final int update( 488 Uri uri, ContentValues values, String selection, String[] selectionArgs) { 489 throw new UnsupportedOperationException("Update not supported"); 490 } 491 492 /** 493 * Implementation is provided by the parent class. Can be overridden to 494 * provide additional functionality, but subclasses <em>must</em> always 495 * call the superclass. If the superclass returns {@code null}, the subclass 496 * may implement custom behavior. 497 * 498 * @see #openDocument(String, String, CancellationSignal) 499 * @see #deleteDocument(String) 500 */ 501 @Override 502 public Bundle call(String method, String arg, Bundle extras) { 503 final Context context = getContext(); 504 505 if (!method.startsWith("android:")) { 506 // Let non-platform methods pass through 507 return super.call(method, arg, extras); 508 } 509 510 final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); 511 final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); 512 513 // Require that caller can manage requested document 514 final boolean callerHasManage = 515 context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS) 516 == PackageManager.PERMISSION_GRANTED; 517 enforceWritePermissionInner(documentUri); 518 519 final Bundle out = new Bundle(); 520 try { 521 if (METHOD_CREATE_DOCUMENT.equals(method)) { 522 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 523 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); 524 525 final String newDocumentId = createDocument(documentId, mimeType, displayName); 526 out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); 527 528 // Extend permission grant towards caller if needed 529 if (!callerHasManage) { 530 final Uri newDocumentUri = DocumentsContract.buildDocumentUri( 531 mAuthority, newDocumentId); 532 context.grantUriPermission(getCallingPackage(), newDocumentUri, 533 Intent.FLAG_GRANT_READ_URI_PERMISSION 534 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 535 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 536 } 537 538 } else if (METHOD_DELETE_DOCUMENT.equals(method)) { 539 deleteDocument(documentId); 540 541 // Document no longer exists, clean up any grants 542 context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION 543 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 544 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 545 546 } else { 547 throw new UnsupportedOperationException("Method not supported " + method); 548 } 549 } catch (FileNotFoundException e) { 550 throw new IllegalStateException("Failed call " + method, e); 551 } 552 return out; 553 } 554 555 /** 556 * Implementation is provided by the parent class. Cannot be overriden. 557 * 558 * @see #openDocument(String, String, CancellationSignal) 559 */ 560 @Override 561 public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 562 return openDocument(getDocumentId(uri), mode, null); 563 } 564 565 /** 566 * Implementation is provided by the parent class. Cannot be overriden. 567 * 568 * @see #openDocument(String, String, CancellationSignal) 569 */ 570 @Override 571 public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 572 throws FileNotFoundException { 573 return openDocument(getDocumentId(uri), mode, signal); 574 } 575 576 /** 577 * Implementation is provided by the parent class. Cannot be overriden. 578 * 579 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 580 */ 581 @Override 582 public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 583 throws FileNotFoundException { 584 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 585 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 586 return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); 587 } else { 588 return super.openTypedAssetFile(uri, mimeTypeFilter, opts); 589 } 590 } 591 592 /** 593 * Implementation is provided by the parent class. Cannot be overriden. 594 * 595 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 596 */ 597 @Override 598 public final AssetFileDescriptor openTypedAssetFile( 599 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 600 throws FileNotFoundException { 601 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 602 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 603 return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); 604 } else { 605 return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); 606 } 607 } 608} 609