DocumentsProvider.java revision 8a2998eade93032a78d681c66ebadbfa6f802f76
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 * 220 * @param projection list of {@link Document} columns to put into the 221 * cursor. If {@code null} all supported columns should be 222 * included. 223 * @see DocumentsContract#EXTRA_LOADING 224 */ 225 @SuppressWarnings("unused") 226 public Cursor queryRecentDocuments(String rootId, String[] projection) 227 throws FileNotFoundException { 228 throw new UnsupportedOperationException("Recent not supported"); 229 } 230 231 /** 232 * Return metadata for the single requested document. You should avoid 233 * making network requests to keep this request fast. 234 * 235 * @param documentId the document to return. 236 * @param projection list of {@link Document} columns to put into the 237 * cursor. If {@code null} all supported columns should be 238 * included. 239 */ 240 public abstract Cursor queryDocument(String documentId, String[] projection) 241 throws FileNotFoundException; 242 243 /** 244 * Return the children documents contained in the requested directory. This 245 * must only return immediate descendants, as additional queries will be 246 * issued to recursively explore the tree. 247 * <p> 248 * If your provider is cloud-based, and you have some data cached or pinned 249 * locally, you may return the local data immediately, setting 250 * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that 251 * you are still fetching additional data. Then, when the network data is 252 * available, you can send a change notification to trigger a requery and 253 * return the complete contents. 254 * <p> 255 * To support change notifications, you must 256 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 257 * Uri, such as 258 * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then 259 * you can call {@link ContentResolver#notifyChange(Uri, 260 * android.database.ContentObserver, boolean)} with that Uri to send change 261 * notifications. 262 * 263 * @param parentDocumentId the directory to return children for. 264 * @param projection list of {@link Document} columns to put into the 265 * cursor. If {@code null} all supported columns should be 266 * included. 267 * @param sortOrder how to order the rows, formatted as an SQL 268 * {@code ORDER BY} clause (excluding the ORDER BY itself). 269 * Passing {@code null} will use the default sort order, which 270 * may be unordered. This ordering is a hint that can be used to 271 * prioritize how data is fetched from the network, but UI may 272 * always enforce a specific ordering. 273 * @see DocumentsContract#EXTRA_LOADING 274 * @see DocumentsContract#EXTRA_INFO 275 * @see DocumentsContract#EXTRA_ERROR 276 */ 277 public abstract Cursor queryChildDocuments( 278 String parentDocumentId, String[] projection, String sortOrder) 279 throws FileNotFoundException; 280 281 /** {@hide} */ 282 @SuppressWarnings("unused") 283 public Cursor queryChildDocumentsForManage( 284 String parentDocumentId, String[] projection, String sortOrder) 285 throws FileNotFoundException { 286 throw new UnsupportedOperationException("Manage not supported"); 287 } 288 289 /** 290 * Return documents that that match the given query under the requested 291 * root. The returned documents should be sorted by relevance in descending 292 * order. How documents are matched against the query string is an 293 * implementation detail left to each provider, but it's suggested that at 294 * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a 295 * case-insensitive fashion. 296 * <p> 297 * Only documents may be returned; directories are not supported in search 298 * results. 299 * <p> 300 * If your provider is cloud-based, and you have some data cached or pinned 301 * locally, you may return the local data immediately, setting 302 * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that 303 * you are still fetching additional data. Then, when the network data is 304 * available, you can send a change notification to trigger a requery and 305 * return the complete contents. 306 * <p> 307 * To support change notifications, you must 308 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 309 * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String, 310 * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri, 311 * android.database.ContentObserver, boolean)} with that Uri to send change 312 * notifications. 313 * 314 * @param rootId the root to search under. 315 * @param query string to match documents against. 316 * @param projection list of {@link Document} columns to put into the 317 * cursor. If {@code null} all supported columns should be 318 * included. 319 * @see DocumentsContract#EXTRA_LOADING 320 * @see DocumentsContract#EXTRA_INFO 321 * @see DocumentsContract#EXTRA_ERROR 322 */ 323 @SuppressWarnings("unused") 324 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 325 throws FileNotFoundException { 326 throw new UnsupportedOperationException("Search not supported"); 327 } 328 329 /** 330 * Return concrete MIME type of the requested document. Must match the value 331 * of {@link Document#COLUMN_MIME_TYPE} for this document. The default 332 * implementation queries {@link #queryDocument(String, String[])}, so 333 * providers may choose to override this as an optimization. 334 */ 335 public String getDocumentType(String documentId) throws FileNotFoundException { 336 final Cursor cursor = queryDocument(documentId, null); 337 try { 338 if (cursor.moveToFirst()) { 339 return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); 340 } else { 341 return null; 342 } 343 } finally { 344 IoUtils.closeQuietly(cursor); 345 } 346 } 347 348 /** 349 * Open and return the requested document. 350 * <p> 351 * Your provider should return a reliable {@link ParcelFileDescriptor} to 352 * detect when the remote caller has finished reading or writing the 353 * document. You may return a pipe or socket pair if the mode is exclusively 354 * "r" or "w", but complex modes like "rw" imply a normal file on disk that 355 * supports seeking. 356 * <p> 357 * If you block while downloading content, you should periodically check 358 * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. 359 * 360 * @param documentId the document to return. 361 * @param mode the mode to open with, such as 'r', 'w', or 'rw'. 362 * @param signal used by the caller to signal if the request should be 363 * cancelled. 364 * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, 365 * OnCloseListener) 366 * @see ParcelFileDescriptor#createReliablePipe() 367 * @see ParcelFileDescriptor#createReliableSocketPair() 368 * @see ParcelFileDescriptor#parseMode(String) 369 */ 370 public abstract ParcelFileDescriptor openDocument( 371 String documentId, String mode, CancellationSignal signal) throws FileNotFoundException; 372 373 /** 374 * Open and return a thumbnail of the requested document. 375 * <p> 376 * A provider should return a thumbnail closely matching the hinted size, 377 * attempting to serve from a local cache if possible. A provider should 378 * never return images more than double the hinted size. 379 * <p> 380 * If you perform expensive operations to download or generate a thumbnail, 381 * you should periodically check {@link CancellationSignal#isCanceled()} to 382 * abort abandoned thumbnail requests. 383 * 384 * @param documentId the document to return. 385 * @param sizeHint hint of the optimal thumbnail dimensions. 386 * @param signal used by the caller to signal if the request should be 387 * cancelled. 388 * @see Document#FLAG_SUPPORTS_THUMBNAIL 389 */ 390 @SuppressWarnings("unused") 391 public AssetFileDescriptor openDocumentThumbnail( 392 String documentId, Point sizeHint, CancellationSignal signal) 393 throws FileNotFoundException { 394 throw new UnsupportedOperationException("Thumbnails not supported"); 395 } 396 397 /** 398 * Implementation is provided by the parent class. Cannot be overriden. 399 * 400 * @see #queryRoots(String[]) 401 * @see #queryRecentDocuments(String, String[]) 402 * @see #queryDocument(String, String[]) 403 * @see #queryChildDocuments(String, String[], String) 404 * @see #querySearchDocuments(String, String, String[]) 405 */ 406 @Override 407 public final Cursor query(Uri uri, String[] projection, String selection, 408 String[] selectionArgs, String sortOrder) { 409 try { 410 switch (mMatcher.match(uri)) { 411 case MATCH_ROOTS: 412 return queryRoots(projection); 413 case MATCH_RECENT: 414 return queryRecentDocuments(getRootId(uri), projection); 415 case MATCH_SEARCH: 416 return querySearchDocuments( 417 getRootId(uri), getSearchDocumentsQuery(uri), projection); 418 case MATCH_DOCUMENT: 419 return queryDocument(getDocumentId(uri), projection); 420 case MATCH_CHILDREN: 421 if (DocumentsContract.isManageMode(uri)) { 422 return queryChildDocumentsForManage( 423 getDocumentId(uri), projection, sortOrder); 424 } else { 425 return queryChildDocuments(getDocumentId(uri), projection, sortOrder); 426 } 427 default: 428 throw new UnsupportedOperationException("Unsupported Uri " + uri); 429 } 430 } catch (FileNotFoundException e) { 431 Log.w(TAG, "Failed during query", e); 432 return null; 433 } 434 } 435 436 /** 437 * Implementation is provided by the parent class. Cannot be overriden. 438 * 439 * @see #getDocumentType(String) 440 */ 441 @Override 442 public final String getType(Uri uri) { 443 try { 444 switch (mMatcher.match(uri)) { 445 case MATCH_ROOT: 446 return DocumentsContract.Root.MIME_TYPE_ITEM; 447 case MATCH_DOCUMENT: 448 return getDocumentType(getDocumentId(uri)); 449 default: 450 return null; 451 } 452 } catch (FileNotFoundException e) { 453 Log.w(TAG, "Failed during getType", e); 454 return null; 455 } 456 } 457 458 /** 459 * Implementation is provided by the parent class. Throws by default, and 460 * cannot be overriden. 461 * 462 * @see #createDocument(String, String, String) 463 */ 464 @Override 465 public final Uri insert(Uri uri, ContentValues values) { 466 throw new UnsupportedOperationException("Insert not supported"); 467 } 468 469 /** 470 * Implementation is provided by the parent class. Throws by default, and 471 * cannot be overriden. 472 * 473 * @see #deleteDocument(String) 474 */ 475 @Override 476 public final int delete(Uri uri, String selection, String[] selectionArgs) { 477 throw new UnsupportedOperationException("Delete not supported"); 478 } 479 480 /** 481 * Implementation is provided by the parent class. Throws by default, and 482 * cannot be overriden. 483 */ 484 @Override 485 public final int update( 486 Uri uri, ContentValues values, String selection, String[] selectionArgs) { 487 throw new UnsupportedOperationException("Update not supported"); 488 } 489 490 /** 491 * Implementation is provided by the parent class. Can be overridden to 492 * provide additional functionality, but subclasses <em>must</em> always 493 * call the superclass. If the superclass returns {@code null}, the subclass 494 * may implement custom behavior. 495 * 496 * @see #openDocument(String, String, CancellationSignal) 497 * @see #deleteDocument(String) 498 */ 499 @Override 500 public Bundle call(String method, String arg, Bundle extras) { 501 final Context context = getContext(); 502 503 if (!method.startsWith("android:")) { 504 // Let non-platform methods pass through 505 return super.call(method, arg, extras); 506 } 507 508 final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); 509 final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); 510 511 // Require that caller can manage requested document 512 final boolean callerHasManage = 513 context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS) 514 == PackageManager.PERMISSION_GRANTED; 515 enforceWritePermissionInner(documentUri); 516 517 final Bundle out = new Bundle(); 518 try { 519 if (METHOD_CREATE_DOCUMENT.equals(method)) { 520 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 521 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); 522 523 final String newDocumentId = createDocument(documentId, mimeType, displayName); 524 out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); 525 526 // Extend permission grant towards caller if needed 527 if (!callerHasManage) { 528 final Uri newDocumentUri = DocumentsContract.buildDocumentUri( 529 mAuthority, newDocumentId); 530 context.grantUriPermission(getCallingPackage(), newDocumentUri, 531 Intent.FLAG_GRANT_READ_URI_PERMISSION 532 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 533 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 534 } 535 536 } else if (METHOD_DELETE_DOCUMENT.equals(method)) { 537 deleteDocument(documentId); 538 539 // Document no longer exists, clean up any grants 540 context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION 541 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 542 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 543 544 } else { 545 throw new UnsupportedOperationException("Method not supported " + method); 546 } 547 } catch (FileNotFoundException e) { 548 throw new IllegalStateException("Failed call " + method, e); 549 } 550 return out; 551 } 552 553 /** 554 * Implementation is provided by the parent class. Cannot be overriden. 555 * 556 * @see #openDocument(String, String, CancellationSignal) 557 */ 558 @Override 559 public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 560 return openDocument(getDocumentId(uri), mode, null); 561 } 562 563 /** 564 * Implementation is provided by the parent class. Cannot be overriden. 565 * 566 * @see #openDocument(String, String, CancellationSignal) 567 */ 568 @Override 569 public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 570 throws FileNotFoundException { 571 return openDocument(getDocumentId(uri), mode, signal); 572 } 573 574 /** 575 * Implementation is provided by the parent class. Cannot be overriden. 576 * 577 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 578 */ 579 @Override 580 public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 581 throws FileNotFoundException { 582 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 583 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 584 return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); 585 } else { 586 return super.openTypedAssetFile(uri, mimeTypeFilter, opts); 587 } 588 } 589 590 /** 591 * Implementation is provided by the parent class. Cannot be overriden. 592 * 593 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 594 */ 595 @Override 596 public final AssetFileDescriptor openTypedAssetFile( 597 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 598 throws FileNotFoundException { 599 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 600 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 601 return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); 602 } else { 603 return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); 604 } 605 } 606} 607