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