DocumentsProvider.java revision 3e1189b3590aefb65a2af720ae2ba959bbd4188d
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.ContentValues; 28import android.content.Context; 29import android.content.Intent; 30import android.content.UriMatcher; 31import android.content.pm.PackageManager; 32import android.content.pm.ProviderInfo; 33import android.content.res.AssetFileDescriptor; 34import android.database.Cursor; 35import android.graphics.Point; 36import android.net.Uri; 37import android.os.Bundle; 38import android.os.CancellationSignal; 39import android.os.ParcelFileDescriptor; 40import android.os.ParcelFileDescriptor.OnCloseListener; 41import android.provider.DocumentsContract.Document; 42import android.util.Log; 43 44import libcore.io.IoUtils; 45 46import java.io.FileNotFoundException; 47 48/** 49 * Base class for a document provider. A document provider should extend this 50 * class and implement the abstract methods. 51 * <p> 52 * Each document provider expresses one or more "roots" which each serve as the 53 * top-level of a tree. For example, a root could represent an account, or a 54 * physical storage device. Under each root, documents are referenced by 55 * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. 56 * <p> 57 * Documents can be either an openable file (with a specific MIME type), or a 58 * directory containing additional documents (with the 59 * {@link Document#MIME_TYPE_DIR} MIME type). Each document can have different 60 * capabilities, as described by {@link Document#COLUMN_FLAGS}. The same 61 * {@link Document#COLUMN_DOCUMENT_ID} can be included in multiple directories. 62 * <p> 63 * Document providers must be protected with the 64 * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can 65 * only be requested by the system. The system-provided UI then issues narrow 66 * Uri permission grants for individual documents when the user explicitly picks 67 * documents. 68 * 69 * @see Intent#ACTION_OPEN_DOCUMENT 70 * @see Intent#ACTION_CREATE_DOCUMENT 71 */ 72public abstract class DocumentsProvider extends ContentProvider { 73 private static final String TAG = "DocumentsProvider"; 74 75 private static final int MATCH_ROOTS = 1; 76 private static final int MATCH_ROOT = 2; 77 private static final int MATCH_RECENT = 3; 78 private static final int MATCH_SEARCH = 4; 79 private static final int MATCH_DOCUMENT = 5; 80 private static final int MATCH_CHILDREN = 6; 81 82 private String mAuthority; 83 84 private UriMatcher mMatcher; 85 86 /** 87 * Implementation is provided by the parent class. 88 */ 89 @Override 90 public void attachInfo(Context context, ProviderInfo info) { 91 mAuthority = info.authority; 92 93 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 94 mMatcher.addURI(mAuthority, "root", MATCH_ROOTS); 95 mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT); 96 mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); 97 mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); 98 mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); 99 mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); 100 101 // Sanity check our setup 102 if (!info.exported) { 103 throw new SecurityException("Provider must be exported"); 104 } 105 if (!info.grantUriPermissions) { 106 throw new SecurityException("Provider must grantUriPermissions"); 107 } 108 if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission) 109 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) { 110 throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS"); 111 } 112 113 super.attachInfo(context, info); 114 } 115 116 /** 117 * Create a new document and return its {@link Document#COLUMN_DOCUMENT_ID}. 118 * A provider must allocate a new {@link Document#COLUMN_DOCUMENT_ID} to 119 * represent the document, which must not change once returned. 120 * 121 * @param documentId the parent directory to create the new document under. 122 * @param mimeType the MIME type associated with the new document. 123 * @param displayName the display name of the new document. 124 */ 125 @SuppressWarnings("unused") 126 public String createDocument(String documentId, String mimeType, String displayName) 127 throws FileNotFoundException { 128 throw new UnsupportedOperationException("Create not supported"); 129 } 130 131 /** 132 * Delete the given document. Upon returning, any Uri permission grants for 133 * the given document will be revoked. If additional documents were deleted 134 * as a side effect of this call, such as documents inside a directory, the 135 * implementor is responsible for revoking those permissions. 136 * 137 * @param documentId the document to delete. 138 */ 139 @SuppressWarnings("unused") 140 public void deleteDocument(String documentId) throws FileNotFoundException { 141 throw new UnsupportedOperationException("Delete not supported"); 142 } 143 144 public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException; 145 146 @SuppressWarnings("unused") 147 public Cursor queryRecentDocuments(String rootId, String[] projection) 148 throws FileNotFoundException { 149 throw new UnsupportedOperationException("Recent not supported"); 150 } 151 152 /** 153 * Return metadata for the given document. A provider should avoid making 154 * network requests to keep this request fast. 155 * 156 * @param documentId the document to return. 157 */ 158 public abstract Cursor queryDocument(String documentId, String[] projection) 159 throws FileNotFoundException; 160 161 /** 162 * Return the children of the given document which is a directory. 163 * 164 * @param parentDocumentId the directory to return children for. 165 */ 166 public abstract Cursor queryChildDocuments( 167 String parentDocumentId, String[] projection, String sortOrder) 168 throws FileNotFoundException; 169 170 /** {@hide} */ 171 @SuppressWarnings("unused") 172 public Cursor queryChildDocumentsForManage( 173 String parentDocumentId, String[] projection, String sortOrder) 174 throws FileNotFoundException { 175 throw new UnsupportedOperationException("Manage not supported"); 176 } 177 178 /** 179 * Return documents that that match the given query. 180 * 181 * @param rootId the root to search under. 182 */ 183 @SuppressWarnings("unused") 184 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 185 throws FileNotFoundException { 186 throw new UnsupportedOperationException("Search not supported"); 187 } 188 189 /** 190 * Return MIME type for the given document. Must match the value of 191 * {@link Document#COLUMN_MIME_TYPE} for this document. 192 */ 193 public String getDocumentType(String documentId) throws FileNotFoundException { 194 final Cursor cursor = queryDocument(documentId, null); 195 try { 196 if (cursor.moveToFirst()) { 197 return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); 198 } else { 199 return null; 200 } 201 } finally { 202 IoUtils.closeQuietly(cursor); 203 } 204 } 205 206 /** 207 * Open and return the requested document. A provider should return a 208 * reliable {@link ParcelFileDescriptor} to detect when the remote caller 209 * has finished reading or writing the document. A provider may return a 210 * pipe or socket pair if the mode is exclusively 211 * {@link ParcelFileDescriptor#MODE_READ_ONLY} or 212 * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like 213 * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on 214 * disk. If a provider blocks while downloading content, it should 215 * periodically check {@link CancellationSignal#isCanceled()} to abort 216 * abandoned open requests. 217 * 218 * @param docId the document to return. 219 * @param mode the mode to open with, such as 'r', 'w', or 'rw'. 220 * @param signal used by the caller to signal if the request should be 221 * cancelled. 222 * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, 223 * OnCloseListener) 224 * @see ParcelFileDescriptor#createReliablePipe() 225 * @see ParcelFileDescriptor#createReliableSocketPair() 226 */ 227 public abstract ParcelFileDescriptor openDocument( 228 String docId, String mode, CancellationSignal signal) throws FileNotFoundException; 229 230 /** 231 * Open and return a thumbnail of the requested document. A provider should 232 * return a thumbnail closely matching the hinted size, attempting to serve 233 * from a local cache if possible. A provider should never return images 234 * more than double the hinted size. If a provider performs expensive 235 * operations to download or generate a thumbnail, it should periodically 236 * check {@link CancellationSignal#isCanceled()} to abort abandoned 237 * thumbnail requests. 238 * 239 * @param docId the document to return. 240 * @param sizeHint hint of the optimal thumbnail dimensions. 241 * @param signal used by the caller to signal if the request should be 242 * cancelled. 243 * @see Document#FLAG_SUPPORTS_THUMBNAIL 244 */ 245 @SuppressWarnings("unused") 246 public AssetFileDescriptor openDocumentThumbnail( 247 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 248 throw new UnsupportedOperationException("Thumbnails not supported"); 249 } 250 251 /** 252 * Implementation is provided by the parent class. Cannot be overriden. 253 * 254 * @see #queryRoots(String[]) 255 * @see #queryRecentDocuments(String, String[]) 256 * @see #queryDocument(String, String[]) 257 * @see #queryChildDocuments(String, String[], String) 258 * @see #querySearchDocuments(String, String, String[]) 259 */ 260 @Override 261 public final Cursor query(Uri uri, String[] projection, String selection, 262 String[] selectionArgs, String sortOrder) { 263 try { 264 switch (mMatcher.match(uri)) { 265 case MATCH_ROOTS: 266 return queryRoots(projection); 267 case MATCH_RECENT: 268 return queryRecentDocuments(getRootId(uri), projection); 269 case MATCH_SEARCH: 270 return querySearchDocuments( 271 getRootId(uri), getSearchDocumentsQuery(uri), projection); 272 case MATCH_DOCUMENT: 273 return queryDocument(getDocumentId(uri), projection); 274 case MATCH_CHILDREN: 275 if (DocumentsContract.isManageMode(uri)) { 276 return queryChildDocumentsForManage( 277 getDocumentId(uri), projection, sortOrder); 278 } else { 279 return queryChildDocuments(getDocumentId(uri), projection, sortOrder); 280 } 281 default: 282 throw new UnsupportedOperationException("Unsupported Uri " + uri); 283 } 284 } catch (FileNotFoundException e) { 285 Log.w(TAG, "Failed during query", e); 286 return null; 287 } 288 } 289 290 /** 291 * Implementation is provided by the parent class. Cannot be overriden. 292 * 293 * @see #getDocumentType(String) 294 */ 295 @Override 296 public final String getType(Uri uri) { 297 try { 298 switch (mMatcher.match(uri)) { 299 case MATCH_ROOT: 300 return DocumentsContract.Root.MIME_TYPE_ITEM; 301 case MATCH_DOCUMENT: 302 return getDocumentType(getDocumentId(uri)); 303 default: 304 return null; 305 } 306 } catch (FileNotFoundException e) { 307 Log.w(TAG, "Failed during getType", e); 308 return null; 309 } 310 } 311 312 /** 313 * Implementation is provided by the parent class. Throws by default, and 314 * cannot be overriden. 315 * 316 * @see #createDocument(String, String, String) 317 */ 318 @Override 319 public final Uri insert(Uri uri, ContentValues values) { 320 throw new UnsupportedOperationException("Insert not supported"); 321 } 322 323 /** 324 * Implementation is provided by the parent class. Throws by default, and 325 * cannot be overriden. 326 * 327 * @see #deleteDocument(String) 328 */ 329 @Override 330 public final int delete(Uri uri, String selection, String[] selectionArgs) { 331 throw new UnsupportedOperationException("Delete not supported"); 332 } 333 334 /** 335 * Implementation is provided by the parent class. Throws by default, and 336 * cannot be overriden. 337 */ 338 @Override 339 public final int update( 340 Uri uri, ContentValues values, String selection, String[] selectionArgs) { 341 throw new UnsupportedOperationException("Update not supported"); 342 } 343 344 /** 345 * Implementation is provided by the parent class. Can be overridden to 346 * provide additional functionality, but subclasses <em>must</em> always 347 * call the superclass. If the superclass returns {@code null}, the subclass 348 * may implement custom behavior. 349 * 350 * @see #openDocument(String, String, CancellationSignal) 351 * @see #deleteDocument(String) 352 */ 353 @Override 354 public Bundle call(String method, String arg, Bundle extras) { 355 final Context context = getContext(); 356 357 if (!method.startsWith("android:")) { 358 // Let non-platform methods pass through 359 return super.call(method, arg, extras); 360 } 361 362 final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); 363 final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); 364 365 // Require that caller can manage given document 366 final boolean callerHasManage = 367 context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS) 368 == PackageManager.PERMISSION_GRANTED; 369 if (!callerHasManage) { 370 getContext().enforceCallingOrSelfUriPermission( 371 documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method); 372 } 373 374 final Bundle out = new Bundle(); 375 try { 376 if (METHOD_CREATE_DOCUMENT.equals(method)) { 377 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 378 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); 379 380 final String newDocumentId = createDocument(documentId, mimeType, displayName); 381 out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); 382 383 // Extend permission grant towards caller if needed 384 if (!callerHasManage) { 385 final Uri newDocumentUri = DocumentsContract.buildDocumentUri( 386 mAuthority, newDocumentId); 387 context.grantUriPermission(getCallingPackage(), newDocumentUri, 388 Intent.FLAG_GRANT_READ_URI_PERMISSION 389 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 390 | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION); 391 } 392 393 } else if (METHOD_DELETE_DOCUMENT.equals(method)) { 394 deleteDocument(documentId); 395 396 // Document no longer exists, clean up any grants 397 context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION 398 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 399 | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION); 400 401 } else { 402 throw new UnsupportedOperationException("Method not supported " + method); 403 } 404 } catch (FileNotFoundException e) { 405 throw new IllegalStateException("Failed call " + method, e); 406 } 407 return out; 408 } 409 410 /** 411 * Implementation is provided by the parent class. 412 * 413 * @see #openDocument(String, String, CancellationSignal) 414 */ 415 @Override 416 public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 417 return openDocument(getDocumentId(uri), mode, null); 418 } 419 420 /** 421 * Implementation is provided by the parent class. 422 * 423 * @see #openDocument(String, String, CancellationSignal) 424 */ 425 @Override 426 public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 427 throws FileNotFoundException { 428 return openDocument(getDocumentId(uri), mode, signal); 429 } 430 431 /** 432 * Implementation is provided by the parent class. 433 * 434 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 435 */ 436 @Override 437 public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 438 throws FileNotFoundException { 439 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 440 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 441 return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); 442 } else { 443 return super.openTypedAssetFile(uri, mimeTypeFilter, opts); 444 } 445 } 446 447 /** 448 * Implementation is provided by the parent class. 449 * 450 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 451 */ 452 @Override 453 public final AssetFileDescriptor openTypedAssetFile( 454 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 455 throws FileNotFoundException { 456 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 457 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 458 return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); 459 } else { 460 return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); 461 } 462 } 463} 464