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.METHOD_COPY_DOCUMENT; 20 import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; 21 import static android.provider.DocumentsContract.METHOD_CREATE_WEB_LINK_INTENT; 22 import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; 23 import static android.provider.DocumentsContract.METHOD_EJECT_ROOT; 24 import static android.provider.DocumentsContract.METHOD_FIND_DOCUMENT_PATH; 25 import static android.provider.DocumentsContract.METHOD_IS_CHILD_DOCUMENT; 26 import static android.provider.DocumentsContract.METHOD_MOVE_DOCUMENT; 27 import static android.provider.DocumentsContract.METHOD_REMOVE_DOCUMENT; 28 import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; 29 import static android.provider.DocumentsContract.buildDocumentUri; 30 import static android.provider.DocumentsContract.buildDocumentUriMaybeUsingTree; 31 import static android.provider.DocumentsContract.buildTreeDocumentUri; 32 import static android.provider.DocumentsContract.getDocumentId; 33 import static android.provider.DocumentsContract.getRootId; 34 import static android.provider.DocumentsContract.getSearchDocumentsQuery; 35 import static android.provider.DocumentsContract.getTreeDocumentId; 36 import static android.provider.DocumentsContract.isTreeUri; 37 38 import android.Manifest; 39 import android.annotation.CallSuper; 40 import android.annotation.Nullable; 41 import android.app.AuthenticationRequiredException; 42 import android.content.ClipDescription; 43 import android.content.ContentProvider; 44 import android.content.ContentResolver; 45 import android.content.ContentValues; 46 import android.content.Context; 47 import android.content.Intent; 48 import android.content.IntentSender; 49 import android.content.UriMatcher; 50 import android.content.pm.PackageManager; 51 import android.content.pm.ProviderInfo; 52 import android.content.res.AssetFileDescriptor; 53 import android.database.Cursor; 54 import android.graphics.Point; 55 import android.net.Uri; 56 import android.os.Bundle; 57 import android.os.CancellationSignal; 58 import android.os.OperationCanceledException; 59 import android.os.ParcelFileDescriptor; 60 import android.os.ParcelFileDescriptor.OnCloseListener; 61 import android.os.ParcelableException; 62 import android.provider.DocumentsContract.Document; 63 import android.provider.DocumentsContract.Path; 64 import android.provider.DocumentsContract.Root; 65 import android.util.Log; 66 67 import libcore.io.IoUtils; 68 69 import java.io.FileNotFoundException; 70 import java.util.LinkedList; 71 import java.util.Objects; 72 73 /** 74 * Base class for a document provider. A document provider offers read and write 75 * access to durable files, such as files stored on a local disk, or files in a 76 * cloud storage service. To create a document provider, extend this class, 77 * implement the abstract methods, and add it to your manifest like this: 78 * 79 * <pre class="prettyprint"><manifest> 80 * ... 81 * <application> 82 * ... 83 * <provider 84 * android:name="com.example.MyCloudProvider" 85 * android:authorities="com.example.mycloudprovider" 86 * android:exported="true" 87 * android:grantUriPermissions="true" 88 * android:permission="android.permission.MANAGE_DOCUMENTS" 89 * android:enabled="@bool/isAtLeastKitKat"> 90 * <intent-filter> 91 * <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> 92 * </intent-filter> 93 * </provider> 94 * ... 95 * </application> 96 *</manifest></pre> 97 * <p> 98 * When defining your provider, you must protect it with 99 * {@link android.Manifest.permission#MANAGE_DOCUMENTS}, which is a permission 100 * only the system can obtain. Applications cannot use a documents provider 101 * directly; they must go through {@link Intent#ACTION_OPEN_DOCUMENT} or 102 * {@link Intent#ACTION_CREATE_DOCUMENT} which requires a user to actively 103 * navigate and select documents. When a user selects documents through that UI, 104 * the system issues narrow URI permission grants to the requesting application. 105 * </p> 106 * <h3>Documents</h3> 107 * <p> 108 * A document can be either an openable stream (with a specific MIME type), or a 109 * directory containing additional documents (with the 110 * {@link Document#MIME_TYPE_DIR} MIME type). Each directory represents the top 111 * of a subtree containing zero or more documents, which can recursively contain 112 * even more documents and directories. 113 * </p> 114 * <p> 115 * Each document can have different capabilities, as described by 116 * {@link Document#COLUMN_FLAGS}. For example, if a document can be represented 117 * as a thumbnail, your provider can set 118 * {@link Document#FLAG_SUPPORTS_THUMBNAIL} and implement 119 * {@link #openDocumentThumbnail(String, Point, CancellationSignal)} to return 120 * that thumbnail. 121 * </p> 122 * <p> 123 * Each document under a provider is uniquely referenced by its 124 * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. A 125 * single document can be included in multiple directories when responding to 126 * {@link #queryChildDocuments(String, String[], String)}. For example, a 127 * provider might surface a single photo in multiple locations: once in a 128 * directory of geographic locations, and again in a directory of dates. 129 * </p> 130 * <h3>Roots</h3> 131 * <p> 132 * All documents are surfaced through one or more "roots." Each root represents 133 * the top of a document tree that a user can navigate. For example, a root 134 * could represent an account or a physical storage device. Similar to 135 * documents, each root can have capabilities expressed through 136 * {@link Root#COLUMN_FLAGS}. 137 * </p> 138 * 139 * @see Intent#ACTION_OPEN_DOCUMENT 140 * @see Intent#ACTION_OPEN_DOCUMENT_TREE 141 * @see Intent#ACTION_CREATE_DOCUMENT 142 */ 143 public abstract class DocumentsProvider extends ContentProvider { 144 private static final String TAG = "DocumentsProvider"; 145 146 private static final int MATCH_ROOTS = 1; 147 private static final int MATCH_ROOT = 2; 148 private static final int MATCH_RECENT = 3; 149 private static final int MATCH_SEARCH = 4; 150 private static final int MATCH_DOCUMENT = 5; 151 private static final int MATCH_CHILDREN = 6; 152 private static final int MATCH_DOCUMENT_TREE = 7; 153 private static final int MATCH_CHILDREN_TREE = 8; 154 155 private String mAuthority; 156 157 private UriMatcher mMatcher; 158 159 /** 160 * Implementation is provided by the parent class. 161 */ 162 @Override 163 public void attachInfo(Context context, ProviderInfo info) { 164 registerAuthority(info.authority); 165 166 // Sanity check our setup 167 if (!info.exported) { 168 throw new SecurityException("Provider must be exported"); 169 } 170 if (!info.grantUriPermissions) { 171 throw new SecurityException("Provider must grantUriPermissions"); 172 } 173 if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission) 174 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) { 175 throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS"); 176 } 177 178 super.attachInfo(context, info); 179 } 180 181 /** {@hide} */ 182 @Override 183 public void attachInfoForTesting(Context context, ProviderInfo info) { 184 registerAuthority(info.authority); 185 186 super.attachInfoForTesting(context, info); 187 } 188 189 private void registerAuthority(String authority) { 190 mAuthority = authority; 191 192 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 193 mMatcher.addURI(mAuthority, "root", MATCH_ROOTS); 194 mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT); 195 mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); 196 mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); 197 mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); 198 mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); 199 mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE); 200 mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE); 201 } 202 203 /** 204 * Test if a document is descendant (child, grandchild, etc) from the given 205 * parent. For example, providers must implement this to support 206 * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. You should avoid making network 207 * requests to keep this request fast. 208 * 209 * @param parentDocumentId parent to verify against. 210 * @param documentId child to verify. 211 * @return if given document is a descendant of the given parent. 212 * @see DocumentsContract.Root#FLAG_SUPPORTS_IS_CHILD 213 */ 214 public boolean isChildDocument(String parentDocumentId, String documentId) { 215 return false; 216 } 217 218 /** {@hide} */ 219 private void enforceTree(Uri documentUri) { 220 if (isTreeUri(documentUri)) { 221 final String parent = getTreeDocumentId(documentUri); 222 final String child = getDocumentId(documentUri); 223 if (Objects.equals(parent, child)) { 224 return; 225 } 226 if (!isChildDocument(parent, child)) { 227 throw new SecurityException( 228 "Document " + child + " is not a descendant of " + parent); 229 } 230 } 231 } 232 233 /** 234 * Create a new document and return its newly generated 235 * {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new 236 * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must 237 * not change once returned. 238 * 239 * @param parentDocumentId the parent directory to create the new document 240 * under. 241 * @param mimeType the concrete MIME type associated with the new document. 242 * If the MIME type is not supported, the provider must throw. 243 * @param displayName the display name of the new document. The provider may 244 * alter this name to meet any internal constraints, such as 245 * avoiding conflicting names. 246 247 * @throws AuthenticationRequiredException If authentication is required from the user (such as 248 * login credentials), but it is not guaranteed that the client will handle this 249 * properly. 250 */ 251 @SuppressWarnings("unused") 252 public String createDocument(String parentDocumentId, String mimeType, String displayName) 253 throws FileNotFoundException { 254 throw new UnsupportedOperationException("Create not supported"); 255 } 256 257 /** 258 * Rename an existing document. 259 * <p> 260 * If a different {@link Document#COLUMN_DOCUMENT_ID} must be used to 261 * represent the renamed document, generate and return it. Any outstanding 262 * URI permission grants will be updated to point at the new document. If 263 * the original {@link Document#COLUMN_DOCUMENT_ID} is still valid after the 264 * rename, return {@code null}. 265 * 266 * @param documentId the document to rename. 267 * @param displayName the updated display name of the document. The provider 268 * may alter this name to meet any internal constraints, such as 269 * avoiding conflicting names. 270 * @throws AuthenticationRequiredException If authentication is required from 271 * the user (such as login credentials), but it is not guaranteed 272 * that the client will handle this properly. 273 */ 274 @SuppressWarnings("unused") 275 public String renameDocument(String documentId, String displayName) 276 throws FileNotFoundException { 277 throw new UnsupportedOperationException("Rename not supported"); 278 } 279 280 /** 281 * Delete the requested document. 282 * <p> 283 * Upon returning, any URI permission grants for the given document will be 284 * revoked. If additional documents were deleted as a side effect of this 285 * call (such as documents inside a directory) the implementor is 286 * responsible for revoking those permissions using 287 * {@link #revokeDocumentPermission(String)}. 288 * 289 * @param documentId the document to delete. 290 * @throws AuthenticationRequiredException If authentication is required from 291 * the user (such as login credentials), but it is not guaranteed 292 * that the client will handle this properly. 293 */ 294 @SuppressWarnings("unused") 295 public void deleteDocument(String documentId) throws FileNotFoundException { 296 throw new UnsupportedOperationException("Delete not supported"); 297 } 298 299 /** 300 * Copy the requested document or a document tree. 301 * <p> 302 * Copies a document including all child documents to another location within 303 * the same document provider. Upon completion returns the document id of 304 * the copied document at the target destination. {@code null} must never 305 * be returned. 306 * 307 * @param sourceDocumentId the document to copy. 308 * @param targetParentDocumentId the target document to be copied into as a child. 309 * @throws AuthenticationRequiredException If authentication is required from 310 * the user (such as login credentials), but it is not guaranteed 311 * that the client will handle this properly. 312 */ 313 @SuppressWarnings("unused") 314 public String copyDocument(String sourceDocumentId, String targetParentDocumentId) 315 throws FileNotFoundException { 316 throw new UnsupportedOperationException("Copy not supported"); 317 } 318 319 /** 320 * Move the requested document or a document tree. 321 * 322 * <p>Moves a document including all child documents to another location within 323 * the same document provider. Upon completion returns the document id of 324 * the copied document at the target destination. {@code null} must never 325 * be returned. 326 * 327 * <p>It's the responsibility of the provider to revoke grants if the document 328 * is no longer accessible using <code>sourceDocumentId</code>. 329 * 330 * @param sourceDocumentId the document to move. 331 * @param sourceParentDocumentId the parent of the document to move. 332 * @param targetParentDocumentId the target document to be a new parent of the 333 * source document. 334 * @throws AuthenticationRequiredException If authentication is required from 335 * the user (such as login credentials), but it is not guaranteed 336 * that the client will handle this properly. 337 */ 338 @SuppressWarnings("unused") 339 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 340 String targetParentDocumentId) 341 throws FileNotFoundException { 342 throw new UnsupportedOperationException("Move not supported"); 343 } 344 345 /** 346 * Removes the requested document or a document tree. 347 * 348 * <p>In contrast to {@link #deleteDocument} it requires specifying the parent. 349 * This method is especially useful if the document can be in multiple parents. 350 * 351 * <p>It's the responsibility of the provider to revoke grants if the document is 352 * removed from the last parent, and effectively the document is deleted. 353 * 354 * @param documentId the document to remove. 355 * @param parentDocumentId the parent of the document to move. 356 * @throws AuthenticationRequiredException If authentication is required from 357 * the user (such as login credentials), but it is not guaranteed 358 * that the client will handle this properly. 359 */ 360 @SuppressWarnings("unused") 361 public void removeDocument(String documentId, String parentDocumentId) 362 throws FileNotFoundException { 363 throw new UnsupportedOperationException("Remove not supported"); 364 } 365 366 /** 367 * Finds the canonical path for the requested document. The path must start 368 * from the parent document if parentDocumentId is not null or the root document 369 * if parentDocumentId is null. If there are more than one path to this document, 370 * return the most typical one. Include both the parent document or root document 371 * and the requested document in the returned path. 372 * 373 * <p>This API assumes that document ID has enough info to infer the root. 374 * Different roots should use different document ID to refer to the same 375 * document. 376 * 377 * 378 * @param parentDocumentId the document from which the path starts if not null, 379 * or null to indicate a path from the root is requested. 380 * @param childDocumentId the document which path is requested. 381 * @return the path of the requested document. If parentDocumentId is null 382 * returned root ID must not be null. If parentDocumentId is not null 383 * returned root ID must be null. 384 * @throws AuthenticationRequiredException If authentication is required from 385 * the user (such as login credentials), but it is not guaranteed 386 * that the client will handle this properly. 387 */ 388 public Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId) 389 throws FileNotFoundException { 390 throw new UnsupportedOperationException("findDocumentPath not supported."); 391 } 392 393 /** 394 * Creates an intent sender for a web link, if the document is web linkable. 395 * <p> 396 * {@link AuthenticationRequiredException} can be thrown if user does not have 397 * sufficient permission for the linked document. Before any new permissions 398 * are granted for the linked document, a visible UI must be shown, so the 399 * user can explicitly confirm whether the permission grants are expected. 400 * The user must be able to cancel the operation. 401 * <p> 402 * Options passed as an argument may include a list of recipients, such 403 * as email addresses. The provider should reflect these options if possible, 404 * but it's acceptable to ignore them. In either case, confirmation UI must 405 * be shown before any new permission grants are granted. 406 * <p> 407 * It is all right to generate a web link without granting new permissions, 408 * if opening the link would result in a page for requesting permission 409 * access. If it's impossible then the operation must fail by throwing an exception. 410 * 411 * @param documentId the document to create a web link intent for. 412 * @param options additional information, such as list of recipients. Optional. 413 * @throws AuthenticationRequiredException If authentication is required from 414 * the user (such as login credentials), but it is not guaranteed 415 * that the client will handle this properly. 416 * 417 * @see DocumentsContract.Document#FLAG_WEB_LINKABLE 418 * @see android.app.PendingIntent#getIntentSender 419 */ 420 public IntentSender createWebLinkIntent(String documentId, @Nullable Bundle options) 421 throws FileNotFoundException { 422 throw new UnsupportedOperationException("createWebLink is not supported."); 423 } 424 425 /** 426 * Return all roots currently provided. To display to users, you must define 427 * at least one root. You should avoid making network requests to keep this 428 * request fast. 429 * <p> 430 * Each root is defined by the metadata columns described in {@link Root}, 431 * including {@link Root#COLUMN_DOCUMENT_ID} which points to a directory 432 * representing a tree of documents to display under that root. 433 * <p> 434 * If this set of roots changes, you must call {@link ContentResolver#notifyChange(Uri, 435 * android.database.ContentObserver, boolean)} with 436 * {@link DocumentsContract#buildRootsUri(String)} to notify the system. 437 * <p> 438 * 439 * @param projection list of {@link Root} columns to put into the cursor. If 440 * {@code null} all supported columns should be included. 441 */ 442 public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException; 443 444 /** 445 * Return recently modified documents under the requested root. This will 446 * only be called for roots that advertise 447 * {@link Root#FLAG_SUPPORTS_RECENTS}. The returned documents should be 448 * sorted by {@link Document#COLUMN_LAST_MODIFIED} in descending order, and 449 * limited to only return the 64 most recently modified documents. 450 * <p> 451 * Recent documents do not support change notifications. 452 * 453 * @param projection list of {@link Document} columns to put into the 454 * cursor. If {@code null} all supported columns should be 455 * included. 456 * @see DocumentsContract#EXTRA_LOADING 457 */ 458 @SuppressWarnings("unused") 459 public Cursor queryRecentDocuments(String rootId, String[] projection) 460 throws FileNotFoundException { 461 throw new UnsupportedOperationException("Recent not supported"); 462 } 463 464 /** 465 * Return metadata for the single requested document. You should avoid 466 * making network requests to keep this request fast. 467 * 468 * @param documentId the document to return. 469 * @param projection list of {@link Document} columns to put into the 470 * cursor. If {@code null} all supported columns should be 471 * included. 472 * @throws AuthenticationRequiredException If authentication is required from 473 * the user (such as login credentials), but it is not guaranteed 474 * that the client will handle this properly. 475 */ 476 public abstract Cursor queryDocument(String documentId, String[] projection) 477 throws FileNotFoundException; 478 479 /** 480 * Return the children documents contained in the requested directory. This 481 * must only return immediate descendants, as additional queries will be 482 * issued to recursively explore the tree. 483 * <p> 484 * Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher 485 * should override {@link #queryChildDocuments(String, String[], Bundle)}. 486 * <p> 487 * If your provider is cloud-based, and you have some data cached or pinned 488 * locally, you may return the local data immediately, setting 489 * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that 490 * you are still fetching additional data. Then, when the network data is 491 * available, you can send a change notification to trigger a requery and 492 * return the complete contents. To return a Cursor with extras, you need to 493 * extend and override {@link Cursor#getExtras()}. 494 * <p> 495 * To support change notifications, you must 496 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 497 * Uri, such as 498 * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then 499 * you can call {@link ContentResolver#notifyChange(Uri, 500 * android.database.ContentObserver, boolean)} with that Uri to send change 501 * notifications. 502 * 503 * @param parentDocumentId the directory to return children for. 504 * @param projection list of {@link Document} columns to put into the 505 * cursor. If {@code null} all supported columns should be 506 * included. 507 * @param sortOrder how to order the rows, formatted as an SQL 508 * {@code ORDER BY} clause (excluding the ORDER BY itself). 509 * Passing {@code null} will use the default sort order, which 510 * may be unordered. This ordering is a hint that can be used to 511 * prioritize how data is fetched from the network, but UI may 512 * always enforce a specific ordering. 513 * @throws AuthenticationRequiredException If authentication is required from 514 * the user (such as login credentials), but it is not guaranteed 515 * that the client will handle this properly. 516 * @see DocumentsContract#EXTRA_LOADING 517 * @see DocumentsContract#EXTRA_INFO 518 * @see DocumentsContract#EXTRA_ERROR 519 */ 520 public abstract Cursor queryChildDocuments( 521 String parentDocumentId, String[] projection, String sortOrder) 522 throws FileNotFoundException; 523 524 /** 525 * Override this method to return the children documents contained 526 * in the requested directory. This must return immediate descendants only. 527 * 528 * <p>If your provider is cloud-based, and you have data cached 529 * locally, you may return the local data immediately, setting 530 * {@link DocumentsContract#EXTRA_LOADING} on Cursor extras to indicate that 531 * you are still fetching additional data. Then, when the network data is 532 * available, you can send a change notification to trigger a requery and 533 * return the complete contents. To return a Cursor with extras, you need to 534 * extend and override {@link Cursor#getExtras()}. 535 * 536 * <p>To support change notifications, you must 537 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 538 * Uri, such as 539 * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then 540 * you can call {@link ContentResolver#notifyChange(Uri, 541 * android.database.ContentObserver, boolean)} with that Uri to send change 542 * notifications. 543 * 544 * @param parentDocumentId the directory to return children for. 545 * @param projection list of {@link Document} columns to put into the 546 * cursor. If {@code null} all supported columns should be 547 * included. 548 * @param queryArgs Bundle containing sorting information or other 549 * argument useful to the provider. If no sorting 550 * information is available, default sorting 551 * will be used, which may be unordered. See 552 * {@link ContentResolver#QUERY_ARG_SORT_COLUMNS} for 553 * details. 554 * @throws AuthenticationRequiredException If authentication is required from 555 * the user (such as login credentials), but it is not guaranteed 556 * that the client will handle this properly. 557 * 558 * @see DocumentsContract#EXTRA_LOADING 559 * @see DocumentsContract#EXTRA_INFO 560 * @see DocumentsContract#EXTRA_ERROR 561 */ 562 public Cursor queryChildDocuments( 563 String parentDocumentId, @Nullable String[] projection, @Nullable Bundle queryArgs) 564 throws FileNotFoundException { 565 566 return queryChildDocuments( 567 parentDocumentId, projection, getSortClause(queryArgs)); 568 } 569 570 /** {@hide} */ 571 @SuppressWarnings("unused") 572 public Cursor queryChildDocumentsForManage( 573 String parentDocumentId, @Nullable String[] projection, @Nullable String sortOrder) 574 throws FileNotFoundException { 575 throw new UnsupportedOperationException("Manage not supported"); 576 } 577 578 /** 579 * Return documents that match the given query under the requested 580 * root. The returned documents should be sorted by relevance in descending 581 * order. How documents are matched against the query string is an 582 * implementation detail left to each provider, but it's suggested that at 583 * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a 584 * case-insensitive fashion. 585 * <p> 586 * If your provider is cloud-based, and you have some data cached or pinned 587 * locally, you may return the local data immediately, setting 588 * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that 589 * you are still fetching additional data. Then, when the network data is 590 * available, you can send a change notification to trigger a requery and 591 * return the complete contents. 592 * <p> 593 * To support change notifications, you must 594 * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant 595 * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String, 596 * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri, 597 * android.database.ContentObserver, boolean)} with that Uri to send change 598 * notifications. 599 * 600 * @param rootId the root to search under. 601 * @param query string to match documents against. 602 * @param projection list of {@link Document} columns to put into the 603 * cursor. If {@code null} all supported columns should be 604 * included. 605 * @throws AuthenticationRequiredException If authentication is required from 606 * the user (such as login credentials), but it is not guaranteed 607 * that the client will handle this properly. 608 * 609 * @see DocumentsContract#EXTRA_LOADING 610 * @see DocumentsContract#EXTRA_INFO 611 * @see DocumentsContract#EXTRA_ERROR 612 */ 613 @SuppressWarnings("unused") 614 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 615 throws FileNotFoundException { 616 throw new UnsupportedOperationException("Search not supported"); 617 } 618 619 /** 620 * Ejects the root. Throws {@link IllegalStateException} if ejection failed. 621 * 622 * @param rootId the root to be ejected. 623 * @see Root#FLAG_SUPPORTS_EJECT 624 */ 625 @SuppressWarnings("unused") 626 public void ejectRoot(String rootId) { 627 throw new UnsupportedOperationException("Eject not supported"); 628 } 629 630 /** 631 * Return concrete MIME type of the requested document. Must match the value 632 * of {@link Document#COLUMN_MIME_TYPE} for this document. The default 633 * implementation queries {@link #queryDocument(String, String[])}, so 634 * providers may choose to override this as an optimization. 635 * <p> 636 * @throws AuthenticationRequiredException If authentication is required from 637 * the user (such as login credentials), but it is not guaranteed 638 * that the client will handle this properly. 639 */ 640 public String getDocumentType(String documentId) throws FileNotFoundException { 641 final Cursor cursor = queryDocument(documentId, null); 642 try { 643 if (cursor.moveToFirst()) { 644 return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); 645 } else { 646 return null; 647 } 648 } finally { 649 IoUtils.closeQuietly(cursor); 650 } 651 } 652 653 /** 654 * Open and return the requested document. 655 * <p> 656 * Your provider should return a reliable {@link ParcelFileDescriptor} to 657 * detect when the remote caller has finished reading or writing the 658 * document. 659 * <p> 660 * Mode "r" should always be supported. Provider should throw 661 * {@link UnsupportedOperationException} if the passing mode is not supported. 662 * You may return a pipe or socket pair if the mode is exclusively "r" or 663 * "w", but complex modes like "rw" imply a normal file on disk that 664 * supports seeking. 665 * <p> 666 * If you block while downloading content, you should periodically check 667 * {@link CancellationSignal#isCanceled()} to abort abandoned open requests. 668 * 669 * @param documentId the document to return. 670 * @param mode the mode to open with, such as 'r', 'w', or 'rw'. 671 * @param signal used by the caller to signal if the request should be 672 * cancelled. May be null. 673 * @throws AuthenticationRequiredException If authentication is required from 674 * the user (such as login credentials), but it is not guaranteed 675 * that the client will handle this properly. 676 * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, 677 * OnCloseListener) 678 * @see ParcelFileDescriptor#createReliablePipe() 679 * @see ParcelFileDescriptor#createReliableSocketPair() 680 * @see ParcelFileDescriptor#parseMode(String) 681 */ 682 public abstract ParcelFileDescriptor openDocument( 683 String documentId, String mode, CancellationSignal signal) throws FileNotFoundException; 684 685 /** 686 * Open and return a thumbnail of the requested document. 687 * <p> 688 * A provider should return a thumbnail closely matching the hinted size, 689 * attempting to serve from a local cache if possible. A provider should 690 * never return images more than double the hinted size. 691 * <p> 692 * If you perform expensive operations to download or generate a thumbnail, 693 * you should periodically check {@link CancellationSignal#isCanceled()} to 694 * abort abandoned thumbnail requests. 695 * 696 * @param documentId the document to return. 697 * @param sizeHint hint of the optimal thumbnail dimensions. 698 * @param signal used by the caller to signal if the request should be 699 * cancelled. May be null. 700 * @throws AuthenticationRequiredException If authentication is required from 701 * the user (such as login credentials), but it is not guaranteed 702 * that the client will handle this properly. 703 * @see Document#FLAG_SUPPORTS_THUMBNAIL 704 */ 705 @SuppressWarnings("unused") 706 public AssetFileDescriptor openDocumentThumbnail( 707 String documentId, Point sizeHint, CancellationSignal signal) 708 throws FileNotFoundException { 709 throw new UnsupportedOperationException("Thumbnails not supported"); 710 } 711 712 /** 713 * Open and return the document in a format matching the specified MIME 714 * type filter. 715 * <p> 716 * A provider may perform a conversion if the documents's MIME type is not 717 * matching the specified MIME type filter. 718 * <p> 719 * Virtual documents must have at least one streamable format. 720 * 721 * @param documentId the document to return. 722 * @param mimeTypeFilter the MIME type filter for the requested format. May 723 * be *\/*, which matches any MIME type. 724 * @param opts extra options from the client. Specific to the content 725 * provider. 726 * @param signal used by the caller to signal if the request should be 727 * cancelled. May be null. 728 * @throws AuthenticationRequiredException If authentication is required from 729 * the user (such as login credentials), but it is not guaranteed 730 * that the client will handle this properly. 731 * @see #getDocumentStreamTypes(String, String) 732 */ 733 @SuppressWarnings("unused") 734 public AssetFileDescriptor openTypedDocument( 735 String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 736 throws FileNotFoundException { 737 throw new FileNotFoundException("The requested MIME type is not supported."); 738 } 739 740 @Override 741 public final Cursor query(Uri uri, String[] projection, String selection, 742 String[] selectionArgs, String sortOrder) { 743 // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary 744 // transport method. We override that, and don't ever delegate to this method. 745 throw new UnsupportedOperationException("Pre-Android-O query format not supported."); 746 } 747 748 /** 749 * WARNING: Sub-classes should not override this method. This method is non-final 750 * solely for the purposes of backwards compatibility. 751 * 752 * @see #queryChildDocuments(String, String[], Bundle), 753 * {@link #queryDocument(String, String[])}, 754 * {@link #queryRecentDocuments(String, String[])}, 755 * {@link #queryRoots(String[])}, and 756 * {@link #querySearchDocuments(String, String, String[])}. 757 */ 758 @Override 759 public Cursor query(Uri uri, String[] projection, String selection, 760 String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { 761 // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary 762 // transport method. We override that, and don't ever delegate to this metohd. 763 throw new UnsupportedOperationException("Pre-Android-O query format not supported."); 764 } 765 766 /** 767 * Implementation is provided by the parent class. Cannot be overriden. 768 * 769 * @see #queryRoots(String[]) 770 * @see #queryRecentDocuments(String, String[]) 771 * @see #queryDocument(String, String[]) 772 * @see #queryChildDocuments(String, String[], String) 773 * @see #querySearchDocuments(String, String, String[]) 774 */ 775 @Override 776 public final Cursor query( 777 Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { 778 try { 779 switch (mMatcher.match(uri)) { 780 case MATCH_ROOTS: 781 return queryRoots(projection); 782 case MATCH_RECENT: 783 return queryRecentDocuments(getRootId(uri), projection); 784 case MATCH_SEARCH: 785 return querySearchDocuments( 786 getRootId(uri), getSearchDocumentsQuery(uri), projection); 787 case MATCH_DOCUMENT: 788 case MATCH_DOCUMENT_TREE: 789 enforceTree(uri); 790 return queryDocument(getDocumentId(uri), projection); 791 case MATCH_CHILDREN: 792 case MATCH_CHILDREN_TREE: 793 enforceTree(uri); 794 if (DocumentsContract.isManageMode(uri)) { 795 // TODO: Update "ForManage" variant to support query args. 796 return queryChildDocumentsForManage( 797 getDocumentId(uri), 798 projection, 799 getSortClause(queryArgs)); 800 } else { 801 return queryChildDocuments(getDocumentId(uri), projection, queryArgs); 802 } 803 default: 804 throw new UnsupportedOperationException("Unsupported Uri " + uri); 805 } 806 } catch (FileNotFoundException e) { 807 Log.w(TAG, "Failed during query", e); 808 return null; 809 } 810 } 811 812 private static @Nullable String getSortClause(@Nullable Bundle queryArgs) { 813 queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY; 814 String sortClause = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER); 815 816 if (sortClause == null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { 817 sortClause = ContentResolver.createSqlSortClause(queryArgs); 818 } 819 820 return sortClause; 821 } 822 823 /** 824 * Implementation is provided by the parent class. Cannot be overriden. 825 * 826 * @see #getDocumentType(String) 827 */ 828 @Override 829 public final String getType(Uri uri) { 830 try { 831 switch (mMatcher.match(uri)) { 832 case MATCH_ROOT: 833 return DocumentsContract.Root.MIME_TYPE_ITEM; 834 case MATCH_DOCUMENT: 835 case MATCH_DOCUMENT_TREE: 836 enforceTree(uri); 837 return getDocumentType(getDocumentId(uri)); 838 default: 839 return null; 840 } 841 } catch (FileNotFoundException e) { 842 Log.w(TAG, "Failed during getType", e); 843 return null; 844 } 845 } 846 847 /** 848 * Implementation is provided by the parent class. Can be overridden to 849 * provide additional functionality, but subclasses <em>must</em> always 850 * call the superclass. If the superclass returns {@code null}, the subclass 851 * may implement custom behavior. 852 * <p> 853 * This is typically used to resolve a subtree URI into a concrete document 854 * reference, issuing a narrower single-document URI permission grant along 855 * the way. 856 * 857 * @see DocumentsContract#buildDocumentUriUsingTree(Uri, String) 858 */ 859 @CallSuper 860 @Override 861 public Uri canonicalize(Uri uri) { 862 final Context context = getContext(); 863 switch (mMatcher.match(uri)) { 864 case MATCH_DOCUMENT_TREE: 865 enforceTree(uri); 866 867 final Uri narrowUri = buildDocumentUri(uri.getAuthority(), getDocumentId(uri)); 868 869 // Caller may only have prefix grant, so extend them a grant to 870 // the narrow URI. 871 final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, uri); 872 context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags); 873 return narrowUri; 874 } 875 return null; 876 } 877 878 private static int getCallingOrSelfUriPermissionModeFlags(Context context, Uri uri) { 879 // TODO: move this to a direct AMS call 880 int modeFlags = 0; 881 if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) 882 == PackageManager.PERMISSION_GRANTED) { 883 modeFlags |= Intent.FLAG_GRANT_READ_URI_PERMISSION; 884 } 885 if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 886 == PackageManager.PERMISSION_GRANTED) { 887 modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 888 } 889 if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION 890 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) 891 == PackageManager.PERMISSION_GRANTED) { 892 modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; 893 } 894 return modeFlags; 895 } 896 897 /** 898 * Implementation is provided by the parent class. Throws by default, and 899 * cannot be overriden. 900 * 901 * @see #createDocument(String, String, String) 902 */ 903 @Override 904 public final Uri insert(Uri uri, ContentValues values) { 905 throw new UnsupportedOperationException("Insert not supported"); 906 } 907 908 /** 909 * Implementation is provided by the parent class. Throws by default, and 910 * cannot be overriden. 911 * 912 * @see #deleteDocument(String) 913 */ 914 @Override 915 public final int delete(Uri uri, String selection, String[] selectionArgs) { 916 throw new UnsupportedOperationException("Delete not supported"); 917 } 918 919 /** 920 * Implementation is provided by the parent class. Throws by default, and 921 * cannot be overriden. 922 */ 923 @Override 924 public final int update( 925 Uri uri, ContentValues values, String selection, String[] selectionArgs) { 926 throw new UnsupportedOperationException("Update not supported"); 927 } 928 929 /** 930 * Implementation is provided by the parent class. Can be overridden to 931 * provide additional functionality, but subclasses <em>must</em> always 932 * call the superclass. If the superclass returns {@code null}, the subclass 933 * may implement custom behavior. 934 */ 935 @CallSuper 936 @Override 937 public Bundle call(String method, String arg, Bundle extras) { 938 if (!method.startsWith("android:")) { 939 // Ignore non-platform methods 940 return super.call(method, arg, extras); 941 } 942 943 try { 944 return callUnchecked(method, arg, extras); 945 } catch (FileNotFoundException e) { 946 throw new ParcelableException(e); 947 } 948 } 949 950 private Bundle callUnchecked(String method, String arg, Bundle extras) 951 throws FileNotFoundException { 952 953 final Context context = getContext(); 954 final Bundle out = new Bundle(); 955 956 if (METHOD_EJECT_ROOT.equals(method)) { 957 // Given that certain system apps can hold MOUNT_UNMOUNT permission, but only apps 958 // signed with platform signature can hold MANAGE_DOCUMENTS, we are going to check for 959 // MANAGE_DOCUMENTS or associated URI permission here instead 960 final Uri rootUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 961 enforceWritePermissionInner(rootUri, getCallingPackage(), null); 962 963 final String rootId = DocumentsContract.getRootId(rootUri); 964 ejectRoot(rootId); 965 966 return out; 967 } 968 969 final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 970 final String authority = documentUri.getAuthority(); 971 final String documentId = DocumentsContract.getDocumentId(documentUri); 972 973 if (!mAuthority.equals(authority)) { 974 throw new SecurityException( 975 "Requested authority " + authority + " doesn't match provider " + mAuthority); 976 } 977 978 // If the URI is a tree URI performs some validation. 979 enforceTree(documentUri); 980 981 if (METHOD_IS_CHILD_DOCUMENT.equals(method)) { 982 enforceReadPermissionInner(documentUri, getCallingPackage(), null); 983 984 final Uri childUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); 985 final String childAuthority = childUri.getAuthority(); 986 final String childId = DocumentsContract.getDocumentId(childUri); 987 988 out.putBoolean( 989 DocumentsContract.EXTRA_RESULT, 990 mAuthority.equals(childAuthority) 991 && isChildDocument(documentId, childId)); 992 993 } else if (METHOD_CREATE_DOCUMENT.equals(method)) { 994 enforceWritePermissionInner(documentUri, getCallingPackage(), null); 995 996 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 997 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); 998 final String newDocumentId = createDocument(documentId, mimeType, displayName); 999 1000 // No need to issue new grants here, since caller either has 1001 // manage permission or a prefix grant. We might generate a 1002 // tree style URI if that's how they called us. 1003 final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, 1004 newDocumentId); 1005 out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); 1006 1007 } else if (METHOD_CREATE_WEB_LINK_INTENT.equals(method)) { 1008 enforceWritePermissionInner(documentUri, getCallingPackage(), null); 1009 1010 final Bundle options = extras.getBundle(DocumentsContract.EXTRA_OPTIONS); 1011 final IntentSender intentSender = createWebLinkIntent(documentId, options); 1012 1013 out.putParcelable(DocumentsContract.EXTRA_RESULT, intentSender); 1014 1015 } else if (METHOD_RENAME_DOCUMENT.equals(method)) { 1016 enforceWritePermissionInner(documentUri, getCallingPackage(), null); 1017 1018 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); 1019 final String newDocumentId = renameDocument(documentId, displayName); 1020 1021 if (newDocumentId != null) { 1022 final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, 1023 newDocumentId); 1024 1025 // If caller came in with a narrow grant, issue them a 1026 // narrow grant for the newly renamed document. 1027 if (!isTreeUri(newDocumentUri)) { 1028 final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, 1029 documentUri); 1030 context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); 1031 } 1032 1033 out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); 1034 1035 // Original document no longer exists, clean up any grants. 1036 revokeDocumentPermission(documentId); 1037 } 1038 1039 } else if (METHOD_DELETE_DOCUMENT.equals(method)) { 1040 enforceWritePermissionInner(documentUri, getCallingPackage(), null); 1041 deleteDocument(documentId); 1042 1043 // Document no longer exists, clean up any grants. 1044 revokeDocumentPermission(documentId); 1045 1046 } else if (METHOD_COPY_DOCUMENT.equals(method)) { 1047 final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); 1048 final String targetId = DocumentsContract.getDocumentId(targetUri); 1049 1050 enforceReadPermissionInner(documentUri, getCallingPackage(), null); 1051 enforceWritePermissionInner(targetUri, getCallingPackage(), null); 1052 1053 final String newDocumentId = copyDocument(documentId, targetId); 1054 1055 if (newDocumentId != null) { 1056 final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, 1057 newDocumentId); 1058 1059 if (!isTreeUri(newDocumentUri)) { 1060 final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, 1061 documentUri); 1062 context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); 1063 } 1064 1065 out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); 1066 } 1067 1068 } else if (METHOD_MOVE_DOCUMENT.equals(method)) { 1069 final Uri parentSourceUri = extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI); 1070 final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri); 1071 final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); 1072 final String targetId = DocumentsContract.getDocumentId(targetUri); 1073 1074 enforceWritePermissionInner(documentUri, getCallingPackage(), null); 1075 enforceReadPermissionInner(parentSourceUri, getCallingPackage(), null); 1076 enforceWritePermissionInner(targetUri, getCallingPackage(), null); 1077 1078 final String newDocumentId = moveDocument(documentId, parentSourceId, targetId); 1079 1080 if (newDocumentId != null) { 1081 final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, 1082 newDocumentId); 1083 1084 if (!isTreeUri(newDocumentUri)) { 1085 final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, 1086 documentUri); 1087 context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); 1088 } 1089 1090 out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); 1091 } 1092 1093 } else if (METHOD_REMOVE_DOCUMENT.equals(method)) { 1094 final Uri parentSourceUri = extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI); 1095 final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri); 1096 1097 enforceReadPermissionInner(parentSourceUri, getCallingPackage(), null); 1098 enforceWritePermissionInner(documentUri, getCallingPackage(), null); 1099 removeDocument(documentId, parentSourceId); 1100 1101 // It's responsibility of the provider to revoke any grants, as the document may be 1102 // still attached to another parents. 1103 } else if (METHOD_FIND_DOCUMENT_PATH.equals(method)) { 1104 final boolean isTreeUri = isTreeUri(documentUri); 1105 1106 if (isTreeUri) { 1107 enforceReadPermissionInner(documentUri, getCallingPackage(), null); 1108 } else { 1109 getContext().enforceCallingPermission(Manifest.permission.MANAGE_DOCUMENTS, null); 1110 } 1111 1112 final String parentDocumentId = isTreeUri 1113 ? DocumentsContract.getTreeDocumentId(documentUri) 1114 : null; 1115 1116 Path path = findDocumentPath(parentDocumentId, documentId); 1117 1118 // Ensure provider doesn't leak information to unprivileged callers. 1119 if (isTreeUri) { 1120 if (!Objects.equals(path.getPath().get(0), parentDocumentId)) { 1121 Log.wtf(TAG, "Provider doesn't return path from the tree root. Expected: " 1122 + parentDocumentId + " found: " + path.getPath().get(0)); 1123 1124 LinkedList<String> docs = new LinkedList<>(path.getPath()); 1125 while (docs.size() > 1 && !Objects.equals(docs.getFirst(), parentDocumentId)) { 1126 docs.removeFirst(); 1127 } 1128 path = new Path(null, docs); 1129 } 1130 1131 if (path.getRootId() != null) { 1132 Log.wtf(TAG, "Provider returns root id :" 1133 + path.getRootId() + " unexpectedly. Erase root id."); 1134 path = new Path(null, path.getPath()); 1135 } 1136 } 1137 1138 out.putParcelable(DocumentsContract.EXTRA_RESULT, path); 1139 } else { 1140 throw new UnsupportedOperationException("Method not supported " + method); 1141 } 1142 1143 return out; 1144 } 1145 1146 /** 1147 * Revoke any active permission grants for the given 1148 * {@link Document#COLUMN_DOCUMENT_ID}, usually called when a document 1149 * becomes invalid. Follows the same semantics as 1150 * {@link Context#revokeUriPermission(Uri, int)}. 1151 */ 1152 public final void revokeDocumentPermission(String documentId) { 1153 final Context context = getContext(); 1154 context.revokeUriPermission(buildDocumentUri(mAuthority, documentId), ~0); 1155 context.revokeUriPermission(buildTreeDocumentUri(mAuthority, documentId), ~0); 1156 } 1157 1158 /** 1159 * Implementation is provided by the parent class. Cannot be overriden. 1160 * 1161 * @see #openDocument(String, String, CancellationSignal) 1162 */ 1163 @Override 1164 public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 1165 enforceTree(uri); 1166 return openDocument(getDocumentId(uri), mode, null); 1167 } 1168 1169 /** 1170 * Implementation is provided by the parent class. Cannot be overriden. 1171 * 1172 * @see #openDocument(String, String, CancellationSignal) 1173 */ 1174 @Override 1175 public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 1176 throws FileNotFoundException { 1177 enforceTree(uri); 1178 return openDocument(getDocumentId(uri), mode, signal); 1179 } 1180 1181 /** 1182 * Implementation is provided by the parent class. Cannot be overriden. 1183 * 1184 * @see #openDocument(String, String, CancellationSignal) 1185 */ 1186 @Override 1187 @SuppressWarnings("resource") 1188 public final AssetFileDescriptor openAssetFile(Uri uri, String mode) 1189 throws FileNotFoundException { 1190 enforceTree(uri); 1191 final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null); 1192 return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; 1193 } 1194 1195 /** 1196 * Implementation is provided by the parent class. Cannot be overriden. 1197 * 1198 * @see #openDocument(String, String, CancellationSignal) 1199 */ 1200 @Override 1201 @SuppressWarnings("resource") 1202 public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal) 1203 throws FileNotFoundException { 1204 enforceTree(uri); 1205 final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal); 1206 return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; 1207 } 1208 1209 /** 1210 * Implementation is provided by the parent class. Cannot be overriden. 1211 * 1212 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 1213 * @see #openTypedDocument(String, String, Bundle, CancellationSignal) 1214 * @see #getDocumentStreamTypes(String, String) 1215 */ 1216 @Override 1217 public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 1218 throws FileNotFoundException { 1219 return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, null); 1220 } 1221 1222 /** 1223 * Implementation is provided by the parent class. Cannot be overriden. 1224 * 1225 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 1226 * @see #openTypedDocument(String, String, Bundle, CancellationSignal) 1227 * @see #getDocumentStreamTypes(String, String) 1228 */ 1229 @Override 1230 public final AssetFileDescriptor openTypedAssetFile( 1231 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 1232 throws FileNotFoundException { 1233 return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, signal); 1234 } 1235 1236 /** 1237 * Return a list of streamable MIME types matching the filter, which can be passed to 1238 * {@link #openTypedDocument(String, String, Bundle, CancellationSignal)}. 1239 * 1240 * <p>The default implementation returns a MIME type provided by 1241 * {@link #queryDocument(String, String[])} as long as it matches the filter and the document 1242 * does not have the {@link Document#FLAG_VIRTUAL_DOCUMENT} flag set. 1243 * 1244 * <p>Virtual documents must have at least one streamable format. 1245 * 1246 * @see #getStreamTypes(Uri, String) 1247 * @see #openTypedDocument(String, String, Bundle, CancellationSignal) 1248 */ 1249 public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) { 1250 Cursor cursor = null; 1251 try { 1252 cursor = queryDocument(documentId, null); 1253 if (cursor.moveToFirst()) { 1254 final String mimeType = 1255 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); 1256 final long flags = 1257 cursor.getLong(cursor.getColumnIndexOrThrow(Document.COLUMN_FLAGS)); 1258 if ((flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0 && mimeType != null && 1259 mimeTypeMatches(mimeTypeFilter, mimeType)) { 1260 return new String[] { mimeType }; 1261 } 1262 } 1263 } catch (FileNotFoundException e) { 1264 return null; 1265 } finally { 1266 IoUtils.closeQuietly(cursor); 1267 } 1268 1269 // No streamable MIME types. 1270 return null; 1271 } 1272 1273 /** 1274 * Called by a client to determine the types of data streams that this content provider 1275 * support for the given URI. 1276 * 1277 * <p>Overriding this method is deprecated. Override {@link #openTypedDocument} instead. 1278 * 1279 * @see #getDocumentStreamTypes(String, String) 1280 */ 1281 @Override 1282 public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { 1283 enforceTree(uri); 1284 return getDocumentStreamTypes(getDocumentId(uri), mimeTypeFilter); 1285 } 1286 1287 /** 1288 * @hide 1289 */ 1290 private final AssetFileDescriptor openTypedAssetFileImpl( 1291 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 1292 throws FileNotFoundException { 1293 enforceTree(uri); 1294 final String documentId = getDocumentId(uri); 1295 if (opts != null && opts.containsKey(ContentResolver.EXTRA_SIZE)) { 1296 final Point sizeHint = opts.getParcelable(ContentResolver.EXTRA_SIZE); 1297 return openDocumentThumbnail(documentId, sizeHint, signal); 1298 } 1299 if ("*/*".equals(mimeTypeFilter)) { 1300 // If they can take anything, the untyped open call is good enough. 1301 return openAssetFile(uri, "r"); 1302 } 1303 final String baseType = getType(uri); 1304 if (baseType != null && ClipDescription.compareMimeTypes(baseType, mimeTypeFilter)) { 1305 // Use old untyped open call if this provider has a type for this 1306 // URI and it matches the request. 1307 return openAssetFile(uri, "r"); 1308 } 1309 // For any other yet unhandled case, let the provider subclass handle it. 1310 return openTypedDocument(documentId, mimeTypeFilter, opts, signal); 1311 } 1312 1313 /** 1314 * @hide 1315 */ 1316 public static boolean mimeTypeMatches(String filter, String test) { 1317 if (test == null) { 1318 return false; 1319 } else if (filter == null || "*/*".equals(filter)) { 1320 return true; 1321 } else if (filter.equals(test)) { 1322 return true; 1323 } else if (filter.endsWith("/*")) { 1324 return filter.regionMatches(0, test, 0, filter.indexOf('/')); 1325 } else { 1326 return false; 1327 } 1328 } 1329 } 1330