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