DocumentsProvider.java revision 4ec973925fc2cd18f9ec0d0ca5af588564fded27
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_DOCUMENT = 4; 79 private static final int MATCH_CHILDREN = 5; 80 private static final int MATCH_SEARCH = 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, "document/*", MATCH_DOCUMENT); 98 mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); 99 mMatcher.addURI(mAuthority, "document/*/search", MATCH_SEARCH); 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, starting the search at 180 * the given directory. 181 * 182 * @param parentDocumentId the directory to start search at. 183 */ 184 @SuppressWarnings("unused") 185 public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection) 186 throws FileNotFoundException { 187 throw new UnsupportedOperationException("Search not supported"); 188 } 189 190 /** 191 * Return MIME type for the given document. Must match the value of 192 * {@link Document#COLUMN_MIME_TYPE} for this document. 193 */ 194 public String getDocumentType(String documentId) throws FileNotFoundException { 195 final Cursor cursor = queryDocument(documentId, null); 196 try { 197 if (cursor.moveToFirst()) { 198 return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); 199 } else { 200 return null; 201 } 202 } finally { 203 IoUtils.closeQuietly(cursor); 204 } 205 } 206 207 /** 208 * Open and return the requested document. A provider should return a 209 * reliable {@link ParcelFileDescriptor} to detect when the remote caller 210 * has finished reading or writing the document. A provider may return a 211 * pipe or socket pair if the mode is exclusively 212 * {@link ParcelFileDescriptor#MODE_READ_ONLY} or 213 * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like 214 * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on 215 * disk. If a provider blocks while downloading content, it should 216 * periodically check {@link CancellationSignal#isCanceled()} to abort 217 * abandoned open requests. 218 * 219 * @param docId the document to return. 220 * @param mode the mode to open with, such as 'r', 'w', or 'rw'. 221 * @param signal used by the caller to signal if the request should be 222 * cancelled. 223 * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, 224 * OnCloseListener) 225 * @see ParcelFileDescriptor#createReliablePipe() 226 * @see ParcelFileDescriptor#createReliableSocketPair() 227 */ 228 public abstract ParcelFileDescriptor openDocument( 229 String docId, String mode, CancellationSignal signal) throws FileNotFoundException; 230 231 /** 232 * Open and return a thumbnail of the requested document. A provider should 233 * return a thumbnail closely matching the hinted size, attempting to serve 234 * from a local cache if possible. A provider should never return images 235 * more than double the hinted size. If a provider performs expensive 236 * operations to download or generate a thumbnail, it should periodically 237 * check {@link CancellationSignal#isCanceled()} to abort abandoned 238 * thumbnail requests. 239 * 240 * @param docId the document to return. 241 * @param sizeHint hint of the optimal thumbnail dimensions. 242 * @param signal used by the caller to signal if the request should be 243 * cancelled. 244 * @see Document#FLAG_SUPPORTS_THUMBNAIL 245 */ 246 @SuppressWarnings("unused") 247 public AssetFileDescriptor openDocumentThumbnail( 248 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 249 throw new UnsupportedOperationException("Thumbnails not supported"); 250 } 251 252 /** 253 * Implementation is provided by the parent class. Cannot be overriden. 254 * 255 * @see #queryRoots(String[]) 256 * @see #queryRecentDocuments(String, String[]) 257 * @see #queryDocument(String, String[]) 258 * @see #queryChildDocuments(String, String[], String) 259 * @see #querySearchDocuments(String, String, String[]) 260 */ 261 @Override 262 public final Cursor query(Uri uri, String[] projection, String selection, 263 String[] selectionArgs, String sortOrder) { 264 try { 265 switch (mMatcher.match(uri)) { 266 case MATCH_ROOTS: 267 return queryRoots(projection); 268 case MATCH_RECENT: 269 return queryRecentDocuments(getRootId(uri), projection); 270 case MATCH_DOCUMENT: 271 return queryDocument(getDocumentId(uri), projection); 272 case MATCH_CHILDREN: 273 if (DocumentsContract.isManageMode(uri)) { 274 return queryChildDocumentsForManage( 275 getDocumentId(uri), projection, sortOrder); 276 } else { 277 return queryChildDocuments(getDocumentId(uri), projection, sortOrder); 278 } 279 case MATCH_SEARCH: 280 return querySearchDocuments( 281 getDocumentId(uri), getSearchDocumentsQuery(uri), projection); 282 default: 283 throw new UnsupportedOperationException("Unsupported Uri " + uri); 284 } 285 } catch (FileNotFoundException e) { 286 Log.w(TAG, "Failed during query", e); 287 return null; 288 } 289 } 290 291 /** 292 * Implementation is provided by the parent class. Cannot be overriden. 293 * 294 * @see #getDocumentType(String) 295 */ 296 @Override 297 public final String getType(Uri uri) { 298 try { 299 switch (mMatcher.match(uri)) { 300 case MATCH_ROOT: 301 return DocumentsContract.Root.MIME_TYPE_ITEM; 302 case MATCH_DOCUMENT: 303 return getDocumentType(getDocumentId(uri)); 304 default: 305 return null; 306 } 307 } catch (FileNotFoundException e) { 308 Log.w(TAG, "Failed during getType", e); 309 return null; 310 } 311 } 312 313 /** 314 * Implementation is provided by the parent class. Throws by default, and 315 * cannot be overriden. 316 * 317 * @see #createDocument(String, String, String) 318 */ 319 @Override 320 public final Uri insert(Uri uri, ContentValues values) { 321 throw new UnsupportedOperationException("Insert not supported"); 322 } 323 324 /** 325 * Implementation is provided by the parent class. Throws by default, and 326 * cannot be overriden. 327 * 328 * @see #deleteDocument(String) 329 */ 330 @Override 331 public final int delete(Uri uri, String selection, String[] selectionArgs) { 332 throw new UnsupportedOperationException("Delete not supported"); 333 } 334 335 /** 336 * Implementation is provided by the parent class. Throws by default, and 337 * cannot be overriden. 338 */ 339 @Override 340 public final int update( 341 Uri uri, ContentValues values, String selection, String[] selectionArgs) { 342 throw new UnsupportedOperationException("Update not supported"); 343 } 344 345 /** 346 * Implementation is provided by the parent class. Can be overridden to 347 * provide additional functionality, but subclasses <em>must</em> always 348 * call the superclass. If the superclass returns {@code null}, the subclass 349 * may implement custom behavior. 350 * 351 * @see #openDocument(String, String, CancellationSignal) 352 * @see #deleteDocument(String) 353 */ 354 @Override 355 public Bundle call(String method, String arg, Bundle extras) { 356 final Context context = getContext(); 357 358 if (!method.startsWith("android:")) { 359 // Let non-platform methods pass through 360 return super.call(method, arg, extras); 361 } 362 363 final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); 364 final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); 365 366 // Require that caller can manage given document 367 final boolean callerHasManage = 368 context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS) 369 == PackageManager.PERMISSION_GRANTED; 370 if (!callerHasManage) { 371 getContext().enforceCallingOrSelfUriPermission( 372 documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method); 373 } 374 375 final Bundle out = new Bundle(); 376 try { 377 if (METHOD_CREATE_DOCUMENT.equals(method)) { 378 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 379 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); 380 381 final String newDocumentId = createDocument(documentId, mimeType, displayName); 382 out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); 383 384 // Extend permission grant towards caller if needed 385 if (!callerHasManage) { 386 final Uri newDocumentUri = DocumentsContract.buildDocumentUri( 387 mAuthority, newDocumentId); 388 context.grantUriPermission(getCallingPackage(), newDocumentUri, 389 Intent.FLAG_GRANT_READ_URI_PERMISSION 390 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 391 | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION); 392 } 393 394 } else if (METHOD_DELETE_DOCUMENT.equals(method)) { 395 deleteDocument(documentId); 396 397 // Document no longer exists, clean up any grants 398 context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION 399 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 400 | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION); 401 402 } else { 403 throw new UnsupportedOperationException("Method not supported " + method); 404 } 405 } catch (FileNotFoundException e) { 406 throw new IllegalStateException("Failed call " + method, e); 407 } 408 return out; 409 } 410 411 /** 412 * Implementation is provided by the parent class. 413 * 414 * @see #openDocument(String, String, CancellationSignal) 415 */ 416 @Override 417 public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 418 return openDocument(getDocumentId(uri), mode, null); 419 } 420 421 /** 422 * Implementation is provided by the parent class. 423 * 424 * @see #openDocument(String, String, CancellationSignal) 425 */ 426 @Override 427 public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 428 throws FileNotFoundException { 429 return openDocument(getDocumentId(uri), mode, signal); 430 } 431 432 /** 433 * Implementation is provided by the parent class. 434 * 435 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 436 */ 437 @Override 438 public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 439 throws FileNotFoundException { 440 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 441 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 442 return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); 443 } else { 444 return super.openTypedAssetFile(uri, mimeTypeFilter, opts); 445 } 446 } 447 448 /** 449 * Implementation is provided by the parent class. 450 * 451 * @see #openDocumentThumbnail(String, Point, CancellationSignal) 452 */ 453 @Override 454 public final AssetFileDescriptor openTypedAssetFile( 455 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 456 throws FileNotFoundException { 457 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 458 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 459 return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); 460 } else { 461 return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); 462 } 463 } 464} 465