DocumentsProvider.java revision aeb16e2435f9975b9fa1fc4b747796647a21292e
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.ACTION_DOCUMENT_ROOT_CHANGED; 20import static android.provider.DocumentsContract.EXTRA_AUTHORITY; 21import static android.provider.DocumentsContract.EXTRA_ROOTS; 22import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE; 23import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; 24import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; 25import static android.provider.DocumentsContract.METHOD_GET_ROOTS; 26import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; 27import static android.provider.DocumentsContract.getDocId; 28import static android.provider.DocumentsContract.getSearchQuery; 29 30import android.content.ContentProvider; 31import android.content.ContentValues; 32import android.content.Context; 33import android.content.Intent; 34import android.content.UriMatcher; 35import android.content.pm.ProviderInfo; 36import android.content.res.AssetFileDescriptor; 37import android.database.Cursor; 38import android.graphics.Point; 39import android.net.Uri; 40import android.os.Bundle; 41import android.os.CancellationSignal; 42import android.os.ParcelFileDescriptor; 43import android.os.ParcelFileDescriptor.OnCloseListener; 44import android.provider.DocumentsContract.DocumentColumns; 45import android.provider.DocumentsContract.DocumentRoot; 46import android.provider.DocumentsContract.Documents; 47import android.util.Log; 48 49import libcore.io.IoUtils; 50 51import java.io.FileNotFoundException; 52import java.util.List; 53 54/** 55 * Base class for a document provider. A document provider should extend this 56 * class and implement the abstract methods. 57 * <p> 58 * Each document provider expresses one or more "roots" which each serve as the 59 * top-level of a tree. For example, a root could represent an account, or a 60 * physical storage device. Under each root, documents are referenced by 61 * {@link DocumentColumns#DOC_ID}, which must not change once returned. 62 * <p> 63 * Documents can be either an openable file (with a specific MIME type), or a 64 * directory containing additional documents (with the 65 * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different 66 * capabilities, as described by {@link DocumentColumns#FLAGS}. The same 67 * {@link DocumentColumns#DOC_ID} can be included in multiple directories. 68 * <p> 69 * Document providers must be protected with the 70 * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can 71 * only be requested by the system. The system-provided UI then issues narrow 72 * Uri permission grants for individual documents when the user explicitly picks 73 * documents. 74 * 75 * @see Intent#ACTION_OPEN_DOCUMENT 76 * @see Intent#ACTION_CREATE_DOCUMENT 77 */ 78public abstract class DocumentsProvider extends ContentProvider { 79 private static final String TAG = "DocumentsProvider"; 80 81 private static final int MATCH_DOCUMENT = 1; 82 private static final int MATCH_CHILDREN = 2; 83 private static final int MATCH_SEARCH = 3; 84 85 private String mAuthority; 86 87 private UriMatcher mMatcher; 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, "docs/*", MATCH_DOCUMENT); 95 mMatcher.addURI(mAuthority, "docs/*/children", MATCH_CHILDREN); 96 mMatcher.addURI(mAuthority, "docs/*/search", MATCH_SEARCH); 97 98 // Sanity check our setup 99 if (!info.exported) { 100 throw new SecurityException("Provider must be exported"); 101 } 102 if (!info.grantUriPermissions) { 103 throw new SecurityException("Provider must grantUriPermissions"); 104 } 105 if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission) 106 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) { 107 throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS"); 108 } 109 110 super.attachInfo(context, info); 111 } 112 113 /** 114 * Return list of all document roots provided by this document provider. 115 * When this list changes, a provider must call 116 * {@link #notifyDocumentRootsChanged()}. 117 */ 118 public abstract List<DocumentRoot> getDocumentRoots(); 119 120 /** 121 * Create and return a new document. A provider must allocate a new 122 * {@link DocumentColumns#DOC_ID} to represent the document, which must not 123 * change once returned. 124 * 125 * @param docId the parent directory to create the new document under. 126 * @param mimeType the MIME type associated with the new document. 127 * @param displayName the display name of the new document. 128 */ 129 @SuppressWarnings("unused") 130 public String createDocument(String docId, String mimeType, String displayName) 131 throws FileNotFoundException { 132 throw new UnsupportedOperationException("Create not supported"); 133 } 134 135 /** 136 * Rename the given document. 137 * 138 * @param docId the document to rename. 139 * @param displayName the new display name. 140 */ 141 @SuppressWarnings("unused") 142 public void renameDocument(String docId, String displayName) throws FileNotFoundException { 143 throw new UnsupportedOperationException("Rename not supported"); 144 } 145 146 /** 147 * Delete the given document. 148 * 149 * @param docId the document to delete. 150 */ 151 @SuppressWarnings("unused") 152 public void deleteDocument(String docId) throws FileNotFoundException { 153 throw new UnsupportedOperationException("Delete not supported"); 154 } 155 156 /** 157 * Return metadata for the given document. A provider should avoid making 158 * network requests to keep this request fast. 159 * 160 * @param docId the document to return. 161 */ 162 public abstract Cursor queryDocument(String docId) throws FileNotFoundException; 163 164 /** 165 * Return the children of the given document which is a directory. 166 * 167 * @param docId the directory to return children for. 168 */ 169 public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException; 170 171 /** 172 * Return documents that that match the given query, starting the search at 173 * the given directory. 174 * 175 * @param docId the directory to start search at. 176 */ 177 @SuppressWarnings("unused") 178 public Cursor querySearch(String docId, String query) throws FileNotFoundException { 179 throw new UnsupportedOperationException("Search not supported"); 180 } 181 182 /** 183 * Return MIME type for the given document. Must match the value of 184 * {@link DocumentColumns#MIME_TYPE} for this document. 185 */ 186 public String getType(String docId) throws FileNotFoundException { 187 final Cursor cursor = queryDocument(docId); 188 try { 189 if (cursor.moveToFirst()) { 190 return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE)); 191 } else { 192 return null; 193 } 194 } finally { 195 IoUtils.closeQuietly(cursor); 196 } 197 } 198 199 /** 200 * Open and return the requested document. A provider should return a 201 * reliable {@link ParcelFileDescriptor} to detect when the remote caller 202 * has finished reading or writing the document. A provider may return a 203 * pipe or socket pair if the mode is exclusively 204 * {@link ParcelFileDescriptor#MODE_READ_ONLY} or 205 * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like 206 * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on 207 * disk. If a provider blocks while downloading content, it should 208 * periodically check {@link CancellationSignal#isCanceled()} to abort 209 * abandoned open requests. 210 * 211 * @param docId the document to return. 212 * @param mode the mode to open with, such as 'r', 'w', or 'rw'. 213 * @param signal used by the caller to signal if the request should be 214 * cancelled. 215 * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, 216 * OnCloseListener) 217 * @see ParcelFileDescriptor#createReliablePipe() 218 * @see ParcelFileDescriptor#createReliableSocketPair() 219 */ 220 public abstract ParcelFileDescriptor openDocument( 221 String docId, String mode, CancellationSignal signal) throws FileNotFoundException; 222 223 /** 224 * Open and return a thumbnail of the requested document. A provider should 225 * return a thumbnail closely matching the hinted size, attempting to serve 226 * from a local cache if possible. A provider should never return images 227 * more than double the hinted size. If a provider performs expensive 228 * operations to download or generate a thumbnail, it should periodically 229 * check {@link CancellationSignal#isCanceled()} to abort abandoned 230 * thumbnail requests. 231 * 232 * @param docId the document to return. 233 * @param sizeHint hint of the optimal thumbnail dimensions. 234 * @param signal used by the caller to signal if the request should be 235 * cancelled. 236 * @see Documents#FLAG_SUPPORTS_THUMBNAIL 237 */ 238 @SuppressWarnings("unused") 239 public AssetFileDescriptor openDocumentThumbnail( 240 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 241 throw new UnsupportedOperationException("Thumbnails not supported"); 242 } 243 244 @Override 245 public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 246 String sortOrder) { 247 try { 248 switch (mMatcher.match(uri)) { 249 case MATCH_DOCUMENT: 250 return queryDocument(getDocId(uri)); 251 case MATCH_CHILDREN: 252 return queryDocumentChildren(getDocId(uri)); 253 case MATCH_SEARCH: 254 return querySearch(getDocId(uri), getSearchQuery(uri)); 255 default: 256 throw new UnsupportedOperationException("Unsupported Uri " + uri); 257 } 258 } catch (FileNotFoundException e) { 259 Log.w(TAG, "Failed during query", e); 260 return null; 261 } 262 } 263 264 @Override 265 public final String getType(Uri uri) { 266 try { 267 switch (mMatcher.match(uri)) { 268 case MATCH_DOCUMENT: 269 return getType(getDocId(uri)); 270 default: 271 return null; 272 } 273 } catch (FileNotFoundException e) { 274 Log.w(TAG, "Failed during getType", e); 275 return null; 276 } 277 } 278 279 @Override 280 public final Uri insert(Uri uri, ContentValues values) { 281 throw new UnsupportedOperationException("Insert not supported"); 282 } 283 284 @Override 285 public final int delete(Uri uri, String selection, String[] selectionArgs) { 286 throw new UnsupportedOperationException("Delete not supported"); 287 } 288 289 @Override 290 public final int update( 291 Uri uri, ContentValues values, String selection, String[] selectionArgs) { 292 throw new UnsupportedOperationException("Update not supported"); 293 } 294 295 @Override 296 public final Bundle callFromPackage( 297 String callingPackage, String method, String arg, Bundle extras) { 298 if (!method.startsWith("android:")) { 299 // Let non-platform methods pass through 300 return super.callFromPackage(callingPackage, method, arg, extras); 301 } 302 303 // Platform operations require the caller explicitly hold manage 304 // permission; Uri permissions don't extend management operations. 305 getContext().enforceCallingOrSelfPermission( 306 android.Manifest.permission.MANAGE_DOCUMENTS, "Document management"); 307 308 final Bundle out = new Bundle(); 309 try { 310 if (METHOD_GET_ROOTS.equals(method)) { 311 final List<DocumentRoot> roots = getDocumentRoots(); 312 out.putParcelableList(EXTRA_ROOTS, roots); 313 314 } else if (METHOD_CREATE_DOCUMENT.equals(method)) { 315 final String docId = extras.getString(DocumentColumns.DOC_ID); 316 final String mimeType = extras.getString(DocumentColumns.MIME_TYPE); 317 final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME); 318 319 // TODO: issue Uri grant towards caller 320 final String newDocId = createDocument(docId, mimeType, displayName); 321 out.putString(DocumentColumns.DOC_ID, newDocId); 322 323 } else if (METHOD_RENAME_DOCUMENT.equals(method)) { 324 final String docId = extras.getString(DocumentColumns.DOC_ID); 325 final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME); 326 renameDocument(docId, displayName); 327 328 } else if (METHOD_DELETE_DOCUMENT.equals(method)) { 329 final String docId = extras.getString(DocumentColumns.DOC_ID); 330 deleteDocument(docId); 331 332 } else { 333 throw new UnsupportedOperationException("Method not supported " + method); 334 } 335 } catch (FileNotFoundException e) { 336 throw new IllegalStateException("Failed call " + method, e); 337 } 338 return out; 339 } 340 341 @Override 342 public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 343 return openDocument(getDocId(uri), mode, null); 344 } 345 346 @Override 347 public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 348 throws FileNotFoundException { 349 return openDocument(getDocId(uri), mode, signal); 350 } 351 352 @Override 353 public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 354 throws FileNotFoundException { 355 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 356 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 357 return openDocumentThumbnail(getDocId(uri), sizeHint, null); 358 } else { 359 return super.openTypedAssetFile(uri, mimeTypeFilter, opts); 360 } 361 } 362 363 @Override 364 public final AssetFileDescriptor openTypedAssetFile( 365 Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 366 throws FileNotFoundException { 367 if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { 368 final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); 369 return openDocumentThumbnail(getDocId(uri), sizeHint, signal); 370 } else { 371 return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); 372 } 373 } 374 375 /** 376 * Notify system that {@link #getDocumentRoots()} has changed, usually due to an 377 * account or device change. 378 */ 379 public void notifyDocumentRootsChanged() { 380 final Intent intent = new Intent(ACTION_DOCUMENT_ROOT_CHANGED); 381 intent.putExtra(EXTRA_AUTHORITY, mAuthority); 382 getContext().sendBroadcast(intent); 383 } 384} 385