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