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 17 package com.android.providers.media; 18 19 import static android.content.ContentResolver.EXTRA_SIZE; 20 import static android.provider.DocumentsContract.QUERY_ARG_DISPLAY_NAME; 21 import static android.provider.DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA; 22 import static android.provider.DocumentsContract.QUERY_ARG_FILE_SIZE_OVER; 23 import static android.provider.DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER; 24 import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES; 25 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.Context; 29 import android.content.res.AssetFileDescriptor; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.database.MatrixCursor.RowBuilder; 33 import android.graphics.Point; 34 import android.media.ExifInterface; 35 import android.media.MediaMetadata; 36 import android.net.Uri; 37 import android.os.Binder; 38 import android.os.Bundle; 39 import android.os.CancellationSignal; 40 import android.os.ParcelFileDescriptor; 41 import android.os.UserHandle; 42 import android.os.UserManager; 43 import android.provider.BaseColumns; 44 import android.provider.DocumentsContract; 45 import android.provider.DocumentsContract.Document; 46 import android.provider.DocumentsContract.Root; 47 import android.provider.DocumentsProvider; 48 import android.provider.MediaStore; 49 import android.provider.MediaStore.Audio; 50 import android.provider.MediaStore.Audio.AlbumColumns; 51 import android.provider.MediaStore.Audio.Albums; 52 import android.provider.MediaStore.Audio.ArtistColumns; 53 import android.provider.MediaStore.Audio.Artists; 54 import android.provider.MediaStore.Audio.AudioColumns; 55 import android.provider.MediaStore.Files; 56 import android.provider.MediaStore.Files.FileColumns; 57 import android.provider.MediaStore.Images; 58 import android.provider.MediaStore.Images.ImageColumns; 59 import android.provider.MediaStore.Video; 60 import android.provider.MediaStore.Video.VideoColumns; 61 import android.text.TextUtils; 62 import android.text.format.DateFormat; 63 import android.text.format.DateUtils; 64 import android.util.Log; 65 import android.util.Pair; 66 67 import androidx.annotation.Nullable; 68 import androidx.core.content.MimeTypeFilter; 69 70 import com.android.providers.media.util.FileUtils; 71 72 import java.io.FileNotFoundException; 73 import java.util.ArrayList; 74 import java.util.Collection; 75 import java.util.HashMap; 76 import java.util.List; 77 import java.util.Locale; 78 import java.util.Map; 79 import java.util.Objects; 80 81 /** 82 * Presents a {@link DocumentsContract} view of {@link MediaProvider} external 83 * contents. 84 */ 85 public class MediaDocumentsProvider extends DocumentsProvider { 86 private static final String TAG = "MediaDocumentsProvider"; 87 88 public static final String AUTHORITY = "com.android.providers.media.documents"; 89 90 private static final String SUPPORTED_QUERY_ARGS = joinNewline( 91 DocumentsContract.QUERY_ARG_DISPLAY_NAME, 92 DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, 93 DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, 94 DocumentsContract.QUERY_ARG_MIME_TYPES); 95 96 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 97 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 98 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_MIME_TYPES, 99 Root.COLUMN_QUERY_ARGS 100 }; 101 102 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 103 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 104 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 105 }; 106 107 private static final String IMAGE_MIME_TYPES = joinNewline("image/*"); 108 109 private static final String VIDEO_MIME_TYPES = joinNewline("video/*"); 110 111 private static final String AUDIO_MIME_TYPES = joinNewline( 112 "audio/*", "application/ogg", "application/x-flac"); 113 114 private static final String DOCUMENT_MIME_TYPES = joinNewline("*/*"); 115 116 static final String TYPE_IMAGES_ROOT = "images_root"; 117 static final String TYPE_IMAGES_BUCKET = "images_bucket"; 118 static final String TYPE_IMAGE = "image"; 119 120 static final String TYPE_VIDEOS_ROOT = "videos_root"; 121 static final String TYPE_VIDEOS_BUCKET = "videos_bucket"; 122 static final String TYPE_VIDEO = "video"; 123 124 static final String TYPE_AUDIO_ROOT = "audio_root"; 125 static final String TYPE_AUDIO = "audio"; 126 static final String TYPE_ARTIST = "artist"; 127 static final String TYPE_ALBUM = "album"; 128 129 static final String TYPE_DOCUMENTS_ROOT = "documents_root"; 130 static final String TYPE_DOCUMENTS_BUCKET = "documents_bucket"; 131 static final String TYPE_DOCUMENT = "document"; 132 133 private static volatile boolean sMediaStoreReady = false; 134 135 private static volatile boolean sReturnedImagesEmpty = false; 136 private static volatile boolean sReturnedVideosEmpty = false; 137 private static volatile boolean sReturnedAudioEmpty = false; 138 private static volatile boolean sReturnedDocumentsEmpty = false; 139 joinNewline(String... args)140 private static String joinNewline(String... args) { 141 return TextUtils.join("\n", args); 142 } 143 144 public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio"; 145 public static final String METADATA_KEY_VIDEO = "android.media.metadata.video"; 146 147 /* 148 * A mapping between media columns and metadata tag names. These keys of the 149 * map form the projection for queries against the media store database. 150 */ 151 private static final Map<String, String> IMAGE_COLUMN_MAP = new HashMap<>(); 152 private static final Map<String, String> VIDEO_COLUMN_MAP = new HashMap<>(); 153 private static final Map<String, String> AUDIO_COLUMN_MAP = new HashMap<>(); 154 155 static { 156 /** 157 * Note that for images (jpegs at least) we'll first try an alternate 158 * means of extracting metadata, one that provides more data. But if 159 * that fails, or if the image type is not JPEG, we fall back to these columns. 160 */ IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH)161 IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH); IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH)162 IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH); IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME)163 IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME); 164 VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION)165 VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION); VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH)166 VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH); VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH)167 VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH); VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE)168 VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE); 169 AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST)170 AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST); AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER)171 AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER); AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM)172 AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM); AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR)173 AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR); AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION)174 AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION); 175 } 176 177 @Override onCreate()178 public boolean onCreate() { 179 notifyRootsChanged(getContext()); 180 return true; 181 } 182 enforceShellRestrictions()183 private void enforceShellRestrictions() { 184 final int callingAppId = UserHandle.getAppId(Binder.getCallingUid()); 185 if (callingAppId == android.os.Process.SHELL_UID 186 && getContext().getSystemService(UserManager.class) 187 .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) { 188 throw new SecurityException( 189 "Shell user cannot access files for user " + UserHandle.myUserId()); 190 } 191 } 192 notifyRootsChanged(Context context)193 private static void notifyRootsChanged(Context context) { 194 context.getContentResolver() 195 .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false); 196 } 197 198 /** 199 * When underlying provider is ready, we kick off a notification of roots 200 * changed so they can be refreshed. 201 */ onMediaStoreReady(Context context, String volumeName)202 static void onMediaStoreReady(Context context, String volumeName) { 203 sMediaStoreReady = true; 204 notifyRootsChanged(context); 205 } 206 207 /** 208 * When inserting the first item of each type, we need to trigger a roots 209 * refresh to clear a previously reported {@link Root#FLAG_EMPTY}. 210 */ onMediaStoreInsert(Context context, String volumeName, int type, long id)211 static void onMediaStoreInsert(Context context, String volumeName, int type, long id) { 212 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) return; 213 214 if (type == FileColumns.MEDIA_TYPE_IMAGE && sReturnedImagesEmpty) { 215 sReturnedImagesEmpty = false; 216 notifyRootsChanged(context); 217 } else if (type == FileColumns.MEDIA_TYPE_VIDEO && sReturnedVideosEmpty) { 218 sReturnedVideosEmpty = false; 219 notifyRootsChanged(context); 220 } else if (type == FileColumns.MEDIA_TYPE_AUDIO && sReturnedAudioEmpty) { 221 sReturnedAudioEmpty = false; 222 notifyRootsChanged(context); 223 } else if (type == FileColumns.MEDIA_TYPE_DOCUMENT && sReturnedDocumentsEmpty) { 224 sReturnedDocumentsEmpty = false; 225 notifyRootsChanged(context); 226 } 227 } 228 229 /** 230 * When deleting an item, we need to revoke any outstanding Uri grants. 231 */ onMediaStoreDelete(Context context, String volumeName, int type, long id)232 static void onMediaStoreDelete(Context context, String volumeName, int type, long id) { 233 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) return; 234 235 if (type == FileColumns.MEDIA_TYPE_IMAGE) { 236 final Uri uri = DocumentsContract.buildDocumentUri( 237 AUTHORITY, getDocIdForIdent(TYPE_IMAGE, id)); 238 context.revokeUriPermission(uri, ~0); 239 notifyRootsChanged(context); 240 } else if (type == FileColumns.MEDIA_TYPE_VIDEO) { 241 final Uri uri = DocumentsContract.buildDocumentUri( 242 AUTHORITY, getDocIdForIdent(TYPE_VIDEO, id)); 243 context.revokeUriPermission(uri, ~0); 244 notifyRootsChanged(context); 245 } else if (type == FileColumns.MEDIA_TYPE_AUDIO) { 246 final Uri uri = DocumentsContract.buildDocumentUri( 247 AUTHORITY, getDocIdForIdent(TYPE_AUDIO, id)); 248 context.revokeUriPermission(uri, ~0); 249 notifyRootsChanged(context); 250 } else if (type == FileColumns.MEDIA_TYPE_DOCUMENT) { 251 final Uri uri = DocumentsContract.buildDocumentUri( 252 AUTHORITY, getDocIdForIdent(TYPE_DOCUMENT, id)); 253 context.revokeUriPermission(uri, ~0); 254 notifyRootsChanged(context); 255 } 256 } 257 258 private static class Ident { 259 public String type; 260 public long id; 261 } 262 getIdentForDocId(String docId)263 private static Ident getIdentForDocId(String docId) { 264 final Ident ident = new Ident(); 265 final int split = docId.indexOf(':'); 266 if (split == -1) { 267 ident.type = docId; 268 ident.id = -1; 269 } else { 270 ident.type = docId.substring(0, split); 271 ident.id = Long.parseLong(docId.substring(split + 1)); 272 } 273 return ident; 274 } 275 getDocIdForIdent(String type, long id)276 private static String getDocIdForIdent(String type, long id) { 277 return type + ":" + id; 278 } 279 resolveRootProjection(String[] projection)280 private static String[] resolveRootProjection(String[] projection) { 281 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 282 } 283 resolveDocumentProjection(String[] projection)284 private static String[] resolveDocumentProjection(String[] projection) { 285 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 286 } 287 buildSearchSelection(String displayName, String[] mimeTypes, long lastModifiedAfter, long fileSizeOver, String columnDisplayName, String columnMimeType, String columnLastModified, String columnFileSize)288 static Pair<String, String[]> buildSearchSelection(String displayName, 289 String[] mimeTypes, long lastModifiedAfter, long fileSizeOver, String columnDisplayName, 290 String columnMimeType, String columnLastModified, String columnFileSize) { 291 StringBuilder selection = new StringBuilder(); 292 final List<String> selectionArgs = new ArrayList<>(); 293 294 if (!displayName.isEmpty()) { 295 selection.append(columnDisplayName + " LIKE ?"); 296 selectionArgs.add("%" + displayName + "%"); 297 } 298 299 if (lastModifiedAfter != -1) { 300 if (selection.length() > 0) { 301 selection.append(" AND "); 302 } 303 304 // The units of DATE_MODIFIED are seconds since 1970. 305 // The units of lastModified are milliseconds since 1970. 306 selection.append(columnLastModified + " > " + lastModifiedAfter / 1000); 307 } 308 309 if (fileSizeOver != -1) { 310 if (selection.length() > 0) { 311 selection.append(" AND "); 312 } 313 314 selection.append(columnFileSize + " > " + fileSizeOver); 315 } 316 317 if (mimeTypes != null && mimeTypes.length > 0) { 318 if (selection.length() > 0) { 319 selection.append(" AND "); 320 } 321 322 selection.append("("); 323 final List<String> tempSelectionArgs = new ArrayList<>(); 324 final StringBuilder tempSelection = new StringBuilder(); 325 List<String> wildcardMimeTypeList = new ArrayList<>(); 326 for (int i = 0; i < mimeTypes.length; ++i) { 327 final String mimeType = mimeTypes[i]; 328 if (!TextUtils.isEmpty(mimeType) && mimeType.endsWith("/*")) { 329 wildcardMimeTypeList.add(mimeType); 330 continue; 331 } 332 333 if (tempSelectionArgs.size() > 0) { 334 tempSelection.append(","); 335 } 336 tempSelection.append("?"); 337 tempSelectionArgs.add(mimeType); 338 } 339 340 for (int i = 0; i < wildcardMimeTypeList.size(); i++) { 341 selection.append(columnMimeType + " LIKE ?") 342 .append((i != wildcardMimeTypeList.size() - 1) ? " OR " : ""); 343 final String mimeType = wildcardMimeTypeList.get(i); 344 selectionArgs.add(mimeType.substring(0, mimeType.length() - 1) + "%"); 345 } 346 347 if (tempSelectionArgs.size() > 0) { 348 if (wildcardMimeTypeList.size() > 0) { 349 selection.append(" OR "); 350 } 351 selection.append(columnMimeType + " IN (") 352 .append(tempSelection.toString()) 353 .append(")"); 354 selectionArgs.addAll(tempSelectionArgs); 355 } 356 357 selection.append(")"); 358 } 359 360 return new Pair<>(selection.toString(), selectionArgs.toArray(new String[0])); 361 } 362 addDocumentSelection(String selection, String[] selectionArgs)363 static Pair<String, String[]> addDocumentSelection(String selection, 364 String[] selectionArgs) { 365 String retSelection = ""; 366 final List<String> retSelectionArgs = new ArrayList<>(); 367 if (!TextUtils.isEmpty(selection) && selectionArgs != null) { 368 retSelection = selection + " AND "; 369 for (int i = 0; i < selectionArgs.length; i++) { 370 retSelectionArgs.add(selectionArgs[i]); 371 } 372 } 373 retSelection += FileColumns.MEDIA_TYPE + "=?"; 374 retSelectionArgs.add("" + FileColumns.MEDIA_TYPE_DOCUMENT); 375 return new Pair<>(retSelection, retSelectionArgs.toArray(new String[0])); 376 } 377 378 /** 379 * Check whether filter mime type and get the matched mime types. 380 * If we don't need to filter mime type, the matchedMimeTypes will be empty. 381 * 382 * @param mimeTypes the mime types to test 383 * @param filter the filter. It is "image/*" or "video/*" or "audio/*". 384 * @param matchedMimeTypes the matched mime types will add into this. 385 * @return true, should do mime type filter. false, no need. 386 */ shouldFilterMimeType(String[] mimeTypes, String filter, List<String> matchedMimeTypes)387 private static boolean shouldFilterMimeType(String[] mimeTypes, String filter, 388 List<String> matchedMimeTypes) { 389 matchedMimeTypes.clear(); 390 boolean shouldQueryMimeType = true; 391 if (mimeTypes != null) { 392 for (int i = 0; i < mimeTypes.length; i++) { 393 // If the mime type is "*/*" or "image/*" or "video/*" or "audio/*", 394 // we don't need to filter mime type. 395 if (TextUtils.equals(mimeTypes[i], "*/*") || 396 TextUtils.equals(mimeTypes[i], filter)) { 397 matchedMimeTypes.clear(); 398 shouldQueryMimeType = false; 399 break; 400 } 401 if (MimeTypeFilter.matches(mimeTypes[i], filter)) { 402 matchedMimeTypes.add(mimeTypes[i]); 403 } 404 } 405 } else { 406 shouldQueryMimeType = false; 407 } 408 409 return shouldQueryMimeType; 410 } 411 getUriForDocumentId(String docId)412 private Uri getUriForDocumentId(String docId) { 413 final Ident ident = getIdentForDocId(docId); 414 if (TYPE_IMAGE.equals(ident.type) && ident.id != -1) { 415 return ContentUris.withAppendedId( 416 Images.Media.EXTERNAL_CONTENT_URI, ident.id); 417 } else if (TYPE_VIDEO.equals(ident.type) && ident.id != -1) { 418 return ContentUris.withAppendedId( 419 Video.Media.EXTERNAL_CONTENT_URI, ident.id); 420 } else if (TYPE_AUDIO.equals(ident.type) && ident.id != -1) { 421 return ContentUris.withAppendedId( 422 Audio.Media.EXTERNAL_CONTENT_URI, ident.id); 423 } else if (TYPE_DOCUMENT.equals(ident.type) && ident.id != -1) { 424 return ContentUris.withAppendedId( 425 Files.EXTERNAL_CONTENT_URI, ident.id); 426 } else { 427 throw new UnsupportedOperationException("Unsupported document " + docId); 428 } 429 } 430 431 @Override deleteDocument(String docId)432 public void deleteDocument(String docId) throws FileNotFoundException { 433 enforceShellRestrictions(); 434 final Uri target = getUriForDocumentId(docId); 435 436 // Delegate to real provider 437 final long token = Binder.clearCallingIdentity(); 438 try { 439 getContext().getContentResolver().delete(target, null, null); 440 } finally { 441 Binder.restoreCallingIdentity(token); 442 } 443 } 444 445 @Override getDocumentMetadata(String docId)446 public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException { 447 enforceShellRestrictions(); 448 return getDocumentMetadataFromIndex(docId); 449 } 450 getDocumentMetadataFromIndex(String docId)451 public @Nullable Bundle getDocumentMetadataFromIndex(String docId) 452 throws FileNotFoundException { 453 454 final Ident ident = getIdentForDocId(docId); 455 456 Map<String, String> columnMap = null; 457 String tagType; 458 Uri query; 459 460 switch (ident.type) { 461 case TYPE_IMAGE: 462 columnMap = IMAGE_COLUMN_MAP; 463 tagType = DocumentsContract.METADATA_EXIF; 464 query = Images.Media.EXTERNAL_CONTENT_URI; 465 break; 466 case TYPE_VIDEO: 467 columnMap = VIDEO_COLUMN_MAP; 468 tagType = METADATA_KEY_VIDEO; 469 query = Video.Media.EXTERNAL_CONTENT_URI; 470 break; 471 case TYPE_AUDIO: 472 columnMap = AUDIO_COLUMN_MAP; 473 tagType = METADATA_KEY_AUDIO; 474 query = Audio.Media.EXTERNAL_CONTENT_URI; 475 break; 476 default: 477 // Unsupported file type. 478 throw new FileNotFoundException( 479 "Metadata request for unsupported file type: " + ident.type); 480 } 481 482 final long token = Binder.clearCallingIdentity(); 483 Cursor cursor = null; 484 Bundle result = null; 485 486 final ContentResolver resolver = getContext().getContentResolver(); 487 Collection<String> columns = columnMap.keySet(); 488 String[] projection = columns.toArray(new String[columns.size()]); 489 try { 490 cursor = resolver.query( 491 query, 492 projection, 493 BaseColumns._ID + "=?", 494 new String[]{Long.toString(ident.id)}, 495 null); 496 497 if (!cursor.moveToFirst()) { 498 throw new FileNotFoundException("Can't find document id: " + docId); 499 } 500 501 final Bundle metadata = extractMetadataFromCursor(cursor, columnMap); 502 result = new Bundle(); 503 result.putBundle(tagType, metadata); 504 result.putStringArray( 505 DocumentsContract.METADATA_TYPES, 506 new String[]{tagType}); 507 } finally { 508 FileUtils.closeQuietly(cursor); 509 Binder.restoreCallingIdentity(token); 510 } 511 return result; 512 } 513 extractMetadataFromCursor(Cursor cursor, Map<String, String> columns)514 private static Bundle extractMetadataFromCursor(Cursor cursor, Map<String, String> columns) { 515 516 assert (cursor.getCount() == 1); 517 518 final Bundle metadata = new Bundle(); 519 for (String col : columns.keySet()) { 520 521 int index = cursor.getColumnIndex(col); 522 String bundleTag = columns.get(col); 523 524 // Special case to be able to pull longs out of a cursor, as long is not a supported 525 // field of getType. 526 if (ExifInterface.TAG_DATETIME.equals(bundleTag)) { 527 if (!cursor.isNull(index)) { 528 // format string to be consistent with how EXIF interface formats the date. 529 long date = cursor.getLong(index); 530 String format = DateFormat.getBestDateTimePattern(Locale.getDefault(), 531 "MMM dd, yyyy, hh:mm"); 532 metadata.putString(bundleTag, DateFormat.format(format, date).toString()); 533 } 534 continue; 535 } 536 537 switch (cursor.getType(index)) { 538 case Cursor.FIELD_TYPE_INTEGER: 539 metadata.putInt(bundleTag, cursor.getInt(index)); 540 break; 541 case Cursor.FIELD_TYPE_FLOAT: 542 //Errors on the side of greater precision since interface doesnt support doubles 543 metadata.putFloat(bundleTag, cursor.getFloat(index)); 544 break; 545 case Cursor.FIELD_TYPE_STRING: 546 metadata.putString(bundleTag, cursor.getString(index)); 547 break; 548 case Cursor.FIELD_TYPE_BLOB: 549 Log.d(TAG, "Unsupported type, blob, for col: " + bundleTag); 550 break; 551 case Cursor.FIELD_TYPE_NULL: 552 Log.d(TAG, "Unsupported type, null, for col: " + bundleTag); 553 break; 554 default: 555 throw new RuntimeException("Data type not supported"); 556 } 557 } 558 559 return metadata; 560 } 561 562 @Override queryRoots(String[] projection)563 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 564 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 565 // Skip all roots when the underlying provider isn't ready yet so that 566 // we avoid triggering an ANR; we'll circle back to notify and refresh 567 // once it's ready 568 if (sMediaStoreReady) { 569 includeImagesRoot(result); 570 includeVideosRoot(result); 571 includeAudioRoot(result); 572 includeDocumentsRoot(result); 573 } 574 return result; 575 } 576 577 @Override queryDocument(String docId, String[] projection)578 public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException { 579 enforceShellRestrictions(); 580 final ContentResolver resolver = getContext().getContentResolver(); 581 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 582 final Ident ident = getIdentForDocId(docId); 583 final String[] queryArgs = new String[] { Long.toString(ident.id) } ; 584 585 final long token = Binder.clearCallingIdentity(); 586 Cursor cursor = null; 587 try { 588 if (TYPE_IMAGES_ROOT.equals(ident.type)) { 589 // single root 590 includeImagesRootDocument(result); 591 } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) { 592 // single bucket 593 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 594 ImagesBucketQuery.PROJECTION, ImageColumns.BUCKET_ID + "=?", 595 queryArgs, ImagesBucketQuery.SORT_ORDER); 596 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); 597 if (cursor.moveToFirst()) { 598 includeImagesBucket(result, cursor); 599 } 600 } else if (TYPE_IMAGE.equals(ident.type)) { 601 // single image 602 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 603 ImageQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs, 604 null); 605 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); 606 if (cursor.moveToFirst()) { 607 includeImage(result, cursor); 608 } 609 } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) { 610 // single root 611 includeVideosRootDocument(result); 612 } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { 613 // single bucket 614 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 615 VideosBucketQuery.PROJECTION, VideoColumns.BUCKET_ID + "=?", 616 queryArgs, VideosBucketQuery.SORT_ORDER); 617 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); 618 if (cursor.moveToFirst()) { 619 includeVideosBucket(result, cursor); 620 } 621 } else if (TYPE_VIDEO.equals(ident.type)) { 622 // single video 623 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 624 VideoQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs, 625 null); 626 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); 627 if (cursor.moveToFirst()) { 628 includeVideo(result, cursor); 629 } 630 } else if (TYPE_AUDIO_ROOT.equals(ident.type)) { 631 // single root 632 includeAudioRootDocument(result); 633 } else if (TYPE_ARTIST.equals(ident.type)) { 634 // single artist 635 cursor = resolver.query(Artists.EXTERNAL_CONTENT_URI, 636 ArtistQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs, 637 null); 638 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 639 if (cursor.moveToFirst()) { 640 includeArtist(result, cursor); 641 } 642 } else if (TYPE_ALBUM.equals(ident.type)) { 643 // single album 644 cursor = resolver.query(Albums.EXTERNAL_CONTENT_URI, 645 AlbumQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs, 646 null); 647 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 648 if (cursor.moveToFirst()) { 649 includeAlbum(result, cursor); 650 } 651 } else if (TYPE_AUDIO.equals(ident.type)) { 652 // single song 653 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, 654 SongQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs, 655 null); 656 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 657 if (cursor.moveToFirst()) { 658 includeAudio(result, cursor); 659 } 660 } else if (TYPE_DOCUMENTS_ROOT.equals(ident.type)) { 661 // single root 662 includeDocumentsRootDocument(result); 663 } else if (TYPE_DOCUMENTS_BUCKET.equals(ident.type)) { 664 // single bucket 665 final Pair<String, String[]> selectionPair = addDocumentSelection( 666 FileColumns.BUCKET_ID + "=?", queryArgs); 667 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentsBucketQuery.PROJECTION, 668 selectionPair.first, selectionPair.second, DocumentsBucketQuery.SORT_ORDER); 669 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); 670 if (cursor.moveToFirst()) { 671 includeDocumentsBucket(result, cursor); 672 } 673 } else if (TYPE_DOCUMENT.equals(ident.type)) { 674 // single document 675 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, 676 FileColumns._ID + "=?", queryArgs, null); 677 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); 678 if (cursor.moveToFirst()) { 679 includeDocument(result, cursor); 680 } 681 } else { 682 throw new UnsupportedOperationException("Unsupported document " + docId); 683 } 684 } finally { 685 FileUtils.closeQuietly(cursor); 686 Binder.restoreCallingIdentity(token); 687 } 688 return result; 689 } 690 691 @Override queryChildDocuments(String docId, String[] projection, String sortOrder)692 public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder) 693 throws FileNotFoundException { 694 enforceShellRestrictions(); 695 final ContentResolver resolver = getContext().getContentResolver(); 696 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 697 final Ident ident = getIdentForDocId(docId); 698 final String[] queryArgs = new String[] { Long.toString(ident.id) } ; 699 700 final long token = Binder.clearCallingIdentity(); 701 Cursor cursor = null; 702 try { 703 if (TYPE_IMAGES_ROOT.equals(ident.type)) { 704 // include all unique buckets 705 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 706 ImagesBucketQuery.PROJECTION, null, null, ImagesBucketQuery.SORT_ORDER); 707 // multiple orders 708 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); 709 long lastId = Long.MIN_VALUE; 710 while (cursor.moveToNext()) { 711 final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID); 712 if (lastId != id) { 713 includeImagesBucket(result, cursor); 714 lastId = id; 715 } 716 } 717 } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) { 718 // include images under bucket 719 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 720 ImageQuery.PROJECTION, ImageColumns.BUCKET_ID + "=?", 721 queryArgs, null); 722 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); 723 while (cursor.moveToNext()) { 724 includeImage(result, cursor); 725 } 726 } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) { 727 // include all unique buckets 728 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 729 VideosBucketQuery.PROJECTION, null, null, VideosBucketQuery.SORT_ORDER); 730 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); 731 long lastId = Long.MIN_VALUE; 732 while (cursor.moveToNext()) { 733 final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID); 734 if (lastId != id) { 735 includeVideosBucket(result, cursor); 736 lastId = id; 737 } 738 } 739 } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { 740 // include videos under bucket 741 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 742 VideoQuery.PROJECTION, VideoColumns.BUCKET_ID + "=?", 743 queryArgs, null); 744 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); 745 while (cursor.moveToNext()) { 746 includeVideo(result, cursor); 747 } 748 } else if (TYPE_AUDIO_ROOT.equals(ident.type)) { 749 // include all artists 750 cursor = resolver.query(Audio.Artists.EXTERNAL_CONTENT_URI, 751 ArtistQuery.PROJECTION, null, null, null); 752 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 753 while (cursor.moveToNext()) { 754 includeArtist(result, cursor); 755 } 756 } else if (TYPE_ARTIST.equals(ident.type)) { 757 // include all albums under artist 758 cursor = resolver.query(Artists.Albums.getContentUri("external", ident.id), 759 AlbumQuery.PROJECTION, null, null, null); 760 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 761 while (cursor.moveToNext()) { 762 includeAlbum(result, cursor); 763 } 764 } else if (TYPE_ALBUM.equals(ident.type)) { 765 // include all songs under album 766 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, 767 SongQuery.PROJECTION, AudioColumns.ALBUM_ID + "=?", 768 queryArgs, null); 769 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 770 while (cursor.moveToNext()) { 771 includeAudio(result, cursor); 772 } 773 } else if (TYPE_DOCUMENTS_ROOT.equals(ident.type)) { 774 // include all unique buckets 775 final Pair<String, String[]> selectionPair = addDocumentSelection(null, null); 776 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentsBucketQuery.PROJECTION, 777 selectionPair.first, selectionPair.second, DocumentsBucketQuery.SORT_ORDER); 778 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); 779 long lastId = Long.MIN_VALUE; 780 while (cursor.moveToNext()) { 781 final long id = cursor.getLong(DocumentsBucketQuery.BUCKET_ID); 782 if (lastId != id) { 783 includeDocumentsBucket(result, cursor); 784 lastId = id; 785 } 786 } 787 } else if (TYPE_DOCUMENTS_BUCKET.equals(ident.type)) { 788 // include documents under bucket 789 final Pair<String, String[]> selectionPair = addDocumentSelection( 790 FileColumns.BUCKET_ID + "=?", queryArgs); 791 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, 792 selectionPair.first, selectionPair.second, null); 793 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); 794 while (cursor.moveToNext()) { 795 includeDocument(result, cursor); 796 } 797 } else { 798 throw new UnsupportedOperationException("Unsupported document " + docId); 799 } 800 } finally { 801 FileUtils.closeQuietly(cursor); 802 Binder.restoreCallingIdentity(token); 803 } 804 return result; 805 } 806 807 @Override queryRecentDocuments(String rootId, String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)808 public Cursor queryRecentDocuments(String rootId, String[] projection, 809 @Nullable Bundle queryArgs, @Nullable CancellationSignal signal) 810 throws FileNotFoundException { 811 enforceShellRestrictions(); 812 final ContentResolver resolver = getContext().getContentResolver(); 813 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 814 815 final long token = Binder.clearCallingIdentity(); 816 817 int limit = -1; 818 if (queryArgs != null) { 819 limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, -1); 820 } 821 if (limit < 0) { 822 // Use default value, and no QUERY_ARG* is honored. 823 limit = 64; 824 } else { 825 // We are honoring the QUERY_ARG_LIMIT. 826 Bundle extras = new Bundle(); 827 result.setExtras(extras); 828 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[]{ 829 ContentResolver.QUERY_ARG_LIMIT 830 }); 831 } 832 833 Cursor cursor = null; 834 try { 835 if (TYPE_IMAGES_ROOT.equals(rootId)) { 836 // include all unique buckets 837 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 838 ImageQuery.PROJECTION, null, null, ImageColumns.DATE_MODIFIED + " DESC"); 839 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); 840 while (cursor.moveToNext() && result.getCount() < limit) { 841 includeImage(result, cursor); 842 } 843 } else if (TYPE_VIDEOS_ROOT.equals(rootId)) { 844 // include all unique buckets 845 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 846 VideoQuery.PROJECTION, null, null, VideoColumns.DATE_MODIFIED + " DESC"); 847 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); 848 while (cursor.moveToNext() && result.getCount() < limit) { 849 includeVideo(result, cursor); 850 } 851 } else if (TYPE_DOCUMENTS_ROOT.equals(rootId)) { 852 // include all unique buckets 853 final Pair<String, String[]> selectionPair = addDocumentSelection(null, null); 854 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, 855 selectionPair.first, selectionPair.second, 856 FileColumns.DATE_MODIFIED + " DESC"); 857 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); 858 while (cursor.moveToNext() && result.getCount() < limit) { 859 includeDocument(result, cursor); 860 } 861 } else { 862 throw new UnsupportedOperationException("Unsupported root " + rootId); 863 } 864 } finally { 865 FileUtils.closeQuietly(cursor); 866 Binder.restoreCallingIdentity(token); 867 } 868 return result; 869 } 870 871 @Override querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)872 public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs) 873 throws FileNotFoundException { 874 enforceShellRestrictions(); 875 final ContentResolver resolver = getContext().getContentResolver(); 876 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 877 878 final long token = Binder.clearCallingIdentity(); 879 880 final String displayName = queryArgs.getString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, 881 "" /* defaultValue */); 882 final long lastModifiedAfter = queryArgs.getLong( 883 DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */); 884 final long fileSizeOver = queryArgs.getLong(DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, 885 -1 /* defaultValue */); 886 final String[] mimeTypes = queryArgs.getStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES); 887 final ArrayList<String> matchedMimeTypes = new ArrayList<>(); 888 889 Cursor cursor = null; 890 try { 891 if (TYPE_IMAGES_ROOT.equals(rootId)) { 892 final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "image/*", 893 matchedMimeTypes); 894 895 // If the queried mime types didn't match the root, we don't need to 896 // query the provider. Ex: the queried mime type is "video/*", but the root 897 // is images root. 898 if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) { 899 final Pair<String, String[]> selectionPair = buildSearchSelection(displayName, 900 matchedMimeTypes.toArray(new String[0]), lastModifiedAfter, 901 fileSizeOver, ImageColumns.DISPLAY_NAME, ImageColumns.MIME_TYPE, 902 ImageColumns.DATE_MODIFIED, ImageColumns.SIZE); 903 904 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 905 ImageQuery.PROJECTION, 906 selectionPair.first, selectionPair.second, 907 ImageColumns.DATE_MODIFIED + " DESC"); 908 909 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); 910 while (cursor.moveToNext()) { 911 includeImage(result, cursor); 912 } 913 } 914 } else if (TYPE_VIDEOS_ROOT.equals(rootId)) { 915 final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "video/*", 916 matchedMimeTypes); 917 918 // If the queried mime types didn't match the root, we don't need to 919 // query the provider. 920 if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) { 921 final Pair<String, String[]> selectionPair = buildSearchSelection(displayName, 922 matchedMimeTypes.toArray(new String[0]), lastModifiedAfter, 923 fileSizeOver, VideoColumns.DISPLAY_NAME, VideoColumns.MIME_TYPE, 924 VideoColumns.DATE_MODIFIED, VideoColumns.SIZE); 925 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, VideoQuery.PROJECTION, 926 selectionPair.first, selectionPair.second, 927 VideoColumns.DATE_MODIFIED + " DESC"); 928 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); 929 while (cursor.moveToNext()) { 930 includeVideo(result, cursor); 931 } 932 } 933 } else if (TYPE_AUDIO_ROOT.equals(rootId)) { 934 final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "audio/*", 935 matchedMimeTypes); 936 937 // If the queried mime types didn't match the root, we don't need to 938 // query the provider. 939 if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) { 940 final Pair<String, String[]> selectionPair = buildSearchSelection(displayName, 941 matchedMimeTypes.toArray(new String[0]), lastModifiedAfter, 942 fileSizeOver, AudioColumns.TITLE, AudioColumns.MIME_TYPE, 943 AudioColumns.DATE_MODIFIED, AudioColumns.SIZE); 944 945 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, SongQuery.PROJECTION, 946 selectionPair.first, selectionPair.second, 947 AudioColumns.DATE_MODIFIED + " DESC"); 948 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); 949 while (cursor.moveToNext()) { 950 includeAudio(result, cursor); 951 } 952 } 953 } else if (TYPE_DOCUMENTS_ROOT.equals(rootId)) { 954 final Pair<String, String[]> initialSelectionPair = buildSearchSelection( 955 displayName, mimeTypes, lastModifiedAfter, fileSizeOver, 956 FileColumns.DISPLAY_NAME, FileColumns.MIME_TYPE, FileColumns.DATE_MODIFIED, 957 FileColumns.SIZE); 958 final Pair<String, String[]> selectionPair = addDocumentSelection( 959 initialSelectionPair.first, initialSelectionPair.second); 960 961 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, 962 selectionPair.first, selectionPair.second, 963 FileColumns.DATE_MODIFIED + " DESC"); 964 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); 965 while (cursor.moveToNext()) { 966 includeDocument(result, cursor); 967 } 968 } else { 969 throw new UnsupportedOperationException("Unsupported root " + rootId); 970 } 971 } finally { 972 FileUtils.closeQuietly(cursor); 973 Binder.restoreCallingIdentity(token); 974 } 975 976 final String[] handledQueryArgs = getHandledQueryArguments(queryArgs); 977 if (handledQueryArgs.length > 0) { 978 final Bundle extras = new Bundle(); 979 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs); 980 result.setExtras(extras); 981 } 982 983 return result; 984 } 985 getHandledQueryArguments(Bundle queryArgs)986 public static String[] getHandledQueryArguments(Bundle queryArgs) { 987 if (queryArgs == null) { 988 return new String[0]; 989 } 990 991 final ArrayList<String> args = new ArrayList<>(); 992 993 if (queryArgs.keySet().contains(QUERY_ARG_EXCLUDE_MEDIA)) { 994 args.add(QUERY_ARG_EXCLUDE_MEDIA); 995 } 996 997 if (queryArgs.keySet().contains(QUERY_ARG_DISPLAY_NAME)) { 998 args.add(QUERY_ARG_DISPLAY_NAME); 999 } 1000 1001 if (queryArgs.keySet().contains(QUERY_ARG_FILE_SIZE_OVER)) { 1002 args.add(QUERY_ARG_FILE_SIZE_OVER); 1003 } 1004 1005 if (queryArgs.keySet().contains(QUERY_ARG_LAST_MODIFIED_AFTER)) { 1006 args.add(QUERY_ARG_LAST_MODIFIED_AFTER); 1007 } 1008 1009 if (queryArgs.keySet().contains(QUERY_ARG_MIME_TYPES)) { 1010 args.add(QUERY_ARG_MIME_TYPES); 1011 } 1012 return args.toArray(new String[0]); 1013 } 1014 1015 @Override openDocument(String docId, String mode, CancellationSignal signal)1016 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) 1017 throws FileNotFoundException { 1018 enforceShellRestrictions(); 1019 final Uri target = getUriForDocumentId(docId); 1020 1021 if (!"r".equals(mode)) { 1022 throw new IllegalArgumentException("Media is read-only"); 1023 } 1024 1025 // Delegate to real provider 1026 final long token = Binder.clearCallingIdentity(); 1027 try { 1028 return getContext().getContentResolver().openFileDescriptor(target, mode); 1029 } finally { 1030 Binder.restoreCallingIdentity(token); 1031 } 1032 } 1033 1034 @Override openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)1035 public AssetFileDescriptor openDocumentThumbnail( 1036 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 1037 enforceShellRestrictions(); 1038 final Ident ident = getIdentForDocId(docId); 1039 1040 final long token = Binder.clearCallingIdentity(); 1041 try { 1042 if (TYPE_IMAGES_BUCKET.equals(ident.type)) { 1043 final long id = getImageForBucketCleared(ident.id); 1044 return openOrCreateImageThumbnailCleared(id, sizeHint, signal); 1045 } else if (TYPE_IMAGE.equals(ident.type)) { 1046 return openOrCreateImageThumbnailCleared(ident.id, sizeHint, signal); 1047 } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { 1048 final long id = getVideoForBucketCleared(ident.id); 1049 return openOrCreateVideoThumbnailCleared(id, sizeHint, signal); 1050 } else if (TYPE_VIDEO.equals(ident.type)) { 1051 return openOrCreateVideoThumbnailCleared(ident.id, sizeHint, signal); 1052 } else { 1053 throw new UnsupportedOperationException("Unsupported document " + docId); 1054 } 1055 } finally { 1056 Binder.restoreCallingIdentity(token); 1057 } 1058 } 1059 isEmpty(Uri uri)1060 private boolean isEmpty(Uri uri) { 1061 final ContentResolver resolver = getContext().getContentResolver(); 1062 final long token = Binder.clearCallingIdentity(); 1063 try (Cursor cursor = resolver.query(uri, 1064 new String[] { "COUNT(_id)" }, null, null, null)) { 1065 if (cursor.moveToFirst()) { 1066 return cursor.getInt(0) == 0; 1067 } else { 1068 // No count information means we need to assume empty 1069 return true; 1070 } 1071 } finally { 1072 Binder.restoreCallingIdentity(token); 1073 } 1074 } 1075 includeImagesRoot(MatrixCursor result)1076 private void includeImagesRoot(MatrixCursor result) { 1077 int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH; 1078 if (isEmpty(Images.Media.EXTERNAL_CONTENT_URI)) { 1079 flags |= Root.FLAG_EMPTY; 1080 sReturnedImagesEmpty = true; 1081 } 1082 1083 final RowBuilder row = result.newRow(); 1084 row.add(Root.COLUMN_ROOT_ID, TYPE_IMAGES_ROOT); 1085 row.add(Root.COLUMN_FLAGS, flags); 1086 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_images)); 1087 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT); 1088 row.add(Root.COLUMN_MIME_TYPES, IMAGE_MIME_TYPES); 1089 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); 1090 } 1091 includeVideosRoot(MatrixCursor result)1092 private void includeVideosRoot(MatrixCursor result) { 1093 int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH; 1094 if (isEmpty(Video.Media.EXTERNAL_CONTENT_URI)) { 1095 flags |= Root.FLAG_EMPTY; 1096 sReturnedVideosEmpty = true; 1097 } 1098 1099 final RowBuilder row = result.newRow(); 1100 row.add(Root.COLUMN_ROOT_ID, TYPE_VIDEOS_ROOT); 1101 row.add(Root.COLUMN_FLAGS, flags); 1102 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_videos)); 1103 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT); 1104 row.add(Root.COLUMN_MIME_TYPES, VIDEO_MIME_TYPES); 1105 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); 1106 } 1107 includeAudioRoot(MatrixCursor result)1108 private void includeAudioRoot(MatrixCursor result) { 1109 int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH; 1110 if (isEmpty(Audio.Media.EXTERNAL_CONTENT_URI)) { 1111 flags |= Root.FLAG_EMPTY; 1112 sReturnedAudioEmpty = true; 1113 } 1114 1115 final RowBuilder row = result.newRow(); 1116 row.add(Root.COLUMN_ROOT_ID, TYPE_AUDIO_ROOT); 1117 row.add(Root.COLUMN_FLAGS, flags); 1118 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_audio)); 1119 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT); 1120 row.add(Root.COLUMN_MIME_TYPES, AUDIO_MIME_TYPES); 1121 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); 1122 } 1123 includeDocumentsRoot(MatrixCursor result)1124 private void includeDocumentsRoot(MatrixCursor result) { 1125 int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH; 1126 if (isEmpty(Files.EXTERNAL_CONTENT_URI)) { 1127 flags |= Root.FLAG_EMPTY; 1128 sReturnedDocumentsEmpty = true; 1129 } 1130 1131 final RowBuilder row = result.newRow(); 1132 row.add(Root.COLUMN_ROOT_ID, TYPE_DOCUMENTS_ROOT); 1133 row.add(Root.COLUMN_FLAGS, flags); 1134 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_documents)); 1135 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_DOCUMENTS_ROOT); 1136 row.add(Root.COLUMN_MIME_TYPES, DOCUMENT_MIME_TYPES); 1137 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); 1138 } 1139 includeImagesRootDocument(MatrixCursor result)1140 private void includeImagesRootDocument(MatrixCursor result) { 1141 final RowBuilder row = result.newRow(); 1142 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT); 1143 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_images)); 1144 row.add(Document.COLUMN_FLAGS, 1145 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 1146 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1147 } 1148 includeVideosRootDocument(MatrixCursor result)1149 private void includeVideosRootDocument(MatrixCursor result) { 1150 final RowBuilder row = result.newRow(); 1151 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT); 1152 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_videos)); 1153 row.add(Document.COLUMN_FLAGS, 1154 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 1155 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1156 } 1157 includeAudioRootDocument(MatrixCursor result)1158 private void includeAudioRootDocument(MatrixCursor result) { 1159 final RowBuilder row = result.newRow(); 1160 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT); 1161 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_audio)); 1162 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1163 } 1164 includeDocumentsRootDocument(MatrixCursor result)1165 private void includeDocumentsRootDocument(MatrixCursor result) { 1166 final RowBuilder row = result.newRow(); 1167 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_DOCUMENTS_ROOT); 1168 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_documents)); 1169 row.add(Document.COLUMN_FLAGS, 1170 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 1171 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1172 } 1173 1174 private interface ImagesBucketQuery { 1175 final String[] PROJECTION = new String[] { 1176 ImageColumns.BUCKET_ID, 1177 ImageColumns.BUCKET_DISPLAY_NAME, 1178 ImageColumns.DATE_MODIFIED, 1179 ImageColumns.VOLUME_NAME }; 1180 final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED 1181 + " DESC"; 1182 1183 final int BUCKET_ID = 0; 1184 final int BUCKET_DISPLAY_NAME = 1; 1185 final int DATE_MODIFIED = 2; 1186 final int VOLUME_NAME = 3; 1187 } 1188 includeImagesBucket(MatrixCursor result, Cursor cursor)1189 private void includeImagesBucket(MatrixCursor result, Cursor cursor) { 1190 final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID); 1191 final String docId = getDocIdForIdent(TYPE_IMAGES_BUCKET, id); 1192 1193 final RowBuilder row = result.newRow(); 1194 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1195 row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName( 1196 cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME), 1197 cursor.getString(ImagesBucketQuery.VOLUME_NAME))); 1198 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1199 row.add(Document.COLUMN_LAST_MODIFIED, 1200 cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1201 row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID 1202 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 1203 } 1204 1205 private interface ImageQuery { 1206 final String[] PROJECTION = new String[] { 1207 ImageColumns._ID, 1208 ImageColumns.DISPLAY_NAME, 1209 ImageColumns.MIME_TYPE, 1210 ImageColumns.SIZE, 1211 ImageColumns.DATE_MODIFIED }; 1212 1213 final int _ID = 0; 1214 final int DISPLAY_NAME = 1; 1215 final int MIME_TYPE = 2; 1216 final int SIZE = 3; 1217 final int DATE_MODIFIED = 4; 1218 } 1219 includeImage(MatrixCursor result, Cursor cursor)1220 private void includeImage(MatrixCursor result, Cursor cursor) { 1221 final long id = cursor.getLong(ImageQuery._ID); 1222 final String docId = getDocIdForIdent(TYPE_IMAGE, id); 1223 1224 final RowBuilder row = result.newRow(); 1225 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1226 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ImageQuery.DISPLAY_NAME)); 1227 row.add(Document.COLUMN_SIZE, cursor.getLong(ImageQuery.SIZE)); 1228 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(ImageQuery.MIME_TYPE)); 1229 row.add(Document.COLUMN_LAST_MODIFIED, 1230 cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1231 row.add(Document.COLUMN_FLAGS, 1232 Document.FLAG_SUPPORTS_THUMBNAIL 1233 | Document.FLAG_SUPPORTS_DELETE 1234 | Document.FLAG_SUPPORTS_METADATA); 1235 } 1236 1237 private interface VideosBucketQuery { 1238 final String[] PROJECTION = new String[] { 1239 VideoColumns.BUCKET_ID, 1240 VideoColumns.BUCKET_DISPLAY_NAME, 1241 VideoColumns.DATE_MODIFIED, 1242 VideoColumns.VOLUME_NAME }; 1243 final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED 1244 + " DESC"; 1245 1246 final int BUCKET_ID = 0; 1247 final int BUCKET_DISPLAY_NAME = 1; 1248 final int DATE_MODIFIED = 2; 1249 final int VOLUME_NAME = 3; 1250 } 1251 includeVideosBucket(MatrixCursor result, Cursor cursor)1252 private void includeVideosBucket(MatrixCursor result, Cursor cursor) { 1253 final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID); 1254 final String docId = getDocIdForIdent(TYPE_VIDEOS_BUCKET, id); 1255 1256 final RowBuilder row = result.newRow(); 1257 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1258 row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName( 1259 cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME), 1260 cursor.getString(VideosBucketQuery.VOLUME_NAME))); 1261 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1262 row.add(Document.COLUMN_LAST_MODIFIED, 1263 cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1264 row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID 1265 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 1266 } 1267 1268 private interface VideoQuery { 1269 final String[] PROJECTION = new String[] { 1270 VideoColumns._ID, 1271 VideoColumns.DISPLAY_NAME, 1272 VideoColumns.MIME_TYPE, 1273 VideoColumns.SIZE, 1274 VideoColumns.DATE_MODIFIED }; 1275 1276 final int _ID = 0; 1277 final int DISPLAY_NAME = 1; 1278 final int MIME_TYPE = 2; 1279 final int SIZE = 3; 1280 final int DATE_MODIFIED = 4; 1281 } 1282 includeVideo(MatrixCursor result, Cursor cursor)1283 private void includeVideo(MatrixCursor result, Cursor cursor) { 1284 final long id = cursor.getLong(VideoQuery._ID); 1285 final String docId = getDocIdForIdent(TYPE_VIDEO, id); 1286 1287 final RowBuilder row = result.newRow(); 1288 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1289 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(VideoQuery.DISPLAY_NAME)); 1290 row.add(Document.COLUMN_SIZE, cursor.getLong(VideoQuery.SIZE)); 1291 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(VideoQuery.MIME_TYPE)); 1292 row.add(Document.COLUMN_LAST_MODIFIED, 1293 cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1294 row.add(Document.COLUMN_FLAGS, 1295 Document.FLAG_SUPPORTS_THUMBNAIL 1296 | Document.FLAG_SUPPORTS_DELETE 1297 | Document.FLAG_SUPPORTS_METADATA); 1298 } 1299 1300 private interface DocumentsBucketQuery { 1301 final String[] PROJECTION = new String[] { 1302 FileColumns.BUCKET_ID, 1303 FileColumns.BUCKET_DISPLAY_NAME, 1304 FileColumns.DATE_MODIFIED, 1305 FileColumns.VOLUME_NAME }; 1306 final String SORT_ORDER = FileColumns.BUCKET_ID + ", " + FileColumns.DATE_MODIFIED 1307 + " DESC"; 1308 1309 final int BUCKET_ID = 0; 1310 final int BUCKET_DISPLAY_NAME = 1; 1311 final int DATE_MODIFIED = 2; 1312 final int VOLUME_NAME = 3; 1313 } 1314 includeDocumentsBucket(MatrixCursor result, Cursor cursor)1315 private void includeDocumentsBucket(MatrixCursor result, Cursor cursor) { 1316 final long id = cursor.getLong(DocumentsBucketQuery.BUCKET_ID); 1317 final String docId = getDocIdForIdent(TYPE_DOCUMENTS_BUCKET, id); 1318 1319 final RowBuilder row = result.newRow(); 1320 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1321 row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName( 1322 cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME), 1323 cursor.getString(DocumentsBucketQuery.VOLUME_NAME))); 1324 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1325 row.add(Document.COLUMN_LAST_MODIFIED, 1326 cursor.getLong(DocumentsBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1327 row.add(Document.COLUMN_FLAGS, 1328 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 1329 } 1330 1331 private interface DocumentQuery { 1332 final String[] PROJECTION = new String[] { 1333 FileColumns._ID, 1334 FileColumns.DISPLAY_NAME, 1335 FileColumns.MIME_TYPE, 1336 FileColumns.SIZE, 1337 FileColumns.DATE_MODIFIED }; 1338 1339 final int _ID = 0; 1340 final int DISPLAY_NAME = 1; 1341 final int MIME_TYPE = 2; 1342 final int SIZE = 3; 1343 final int DATE_MODIFIED = 4; 1344 } 1345 includeDocument(MatrixCursor result, Cursor cursor)1346 private void includeDocument(MatrixCursor result, Cursor cursor) { 1347 final long id = cursor.getLong(DocumentQuery._ID); 1348 final String docId = getDocIdForIdent(TYPE_DOCUMENT, id); 1349 1350 final RowBuilder row = result.newRow(); 1351 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1352 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(DocumentQuery.DISPLAY_NAME)); 1353 row.add(Document.COLUMN_SIZE, cursor.getLong(DocumentQuery.SIZE)); 1354 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(DocumentQuery.MIME_TYPE)); 1355 row.add(Document.COLUMN_LAST_MODIFIED, 1356 cursor.getLong(DocumentQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1357 row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE); 1358 } 1359 1360 private interface ArtistQuery { 1361 final String[] PROJECTION = new String[] { 1362 BaseColumns._ID, 1363 ArtistColumns.ARTIST }; 1364 1365 final int _ID = 0; 1366 final int ARTIST = 1; 1367 } 1368 includeArtist(MatrixCursor result, Cursor cursor)1369 private void includeArtist(MatrixCursor result, Cursor cursor) { 1370 final long id = cursor.getLong(ArtistQuery._ID); 1371 final String docId = getDocIdForIdent(TYPE_ARTIST, id); 1372 1373 final RowBuilder row = result.newRow(); 1374 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1375 row.add(Document.COLUMN_DISPLAY_NAME, 1376 cleanUpMediaDisplayName(cursor.getString(ArtistQuery.ARTIST))); 1377 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1378 } 1379 1380 private interface AlbumQuery { 1381 final String[] PROJECTION = new String[] { 1382 AlbumColumns.ALBUM_ID, 1383 AlbumColumns.ALBUM }; 1384 1385 final int ALBUM_ID = 0; 1386 final int ALBUM = 1; 1387 } 1388 includeAlbum(MatrixCursor result, Cursor cursor)1389 private void includeAlbum(MatrixCursor result, Cursor cursor) { 1390 final long id = cursor.getLong(AlbumQuery.ALBUM_ID); 1391 final String docId = getDocIdForIdent(TYPE_ALBUM, id); 1392 1393 final RowBuilder row = result.newRow(); 1394 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1395 row.add(Document.COLUMN_DISPLAY_NAME, 1396 cleanUpMediaDisplayName(cursor.getString(AlbumQuery.ALBUM))); 1397 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 1398 } 1399 1400 private interface SongQuery { 1401 final String[] PROJECTION = new String[] { 1402 AudioColumns._ID, 1403 AudioColumns.DISPLAY_NAME, 1404 AudioColumns.MIME_TYPE, 1405 AudioColumns.SIZE, 1406 AudioColumns.DATE_MODIFIED }; 1407 1408 final int _ID = 0; 1409 final int DISPLAY_NAME = 1; 1410 final int MIME_TYPE = 2; 1411 final int SIZE = 3; 1412 final int DATE_MODIFIED = 4; 1413 } 1414 includeAudio(MatrixCursor result, Cursor cursor)1415 private void includeAudio(MatrixCursor result, Cursor cursor) { 1416 final long id = cursor.getLong(SongQuery._ID); 1417 final String docId = getDocIdForIdent(TYPE_AUDIO, id); 1418 1419 final RowBuilder row = result.newRow(); 1420 row.add(Document.COLUMN_DOCUMENT_ID, docId); 1421 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(SongQuery.DISPLAY_NAME)); 1422 row.add(Document.COLUMN_SIZE, cursor.getLong(SongQuery.SIZE)); 1423 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE)); 1424 row.add(Document.COLUMN_LAST_MODIFIED, 1425 cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 1426 row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE 1427 | Document.FLAG_SUPPORTS_METADATA); 1428 } 1429 1430 private interface ImagesBucketThumbnailQuery { 1431 final String[] PROJECTION = new String[] { 1432 ImageColumns._ID, 1433 ImageColumns.BUCKET_ID, 1434 ImageColumns.DATE_MODIFIED }; 1435 1436 final int _ID = 0; 1437 final int BUCKET_ID = 1; 1438 final int DATE_MODIFIED = 2; 1439 } 1440 getImageForBucketCleared(long bucketId)1441 private long getImageForBucketCleared(long bucketId) throws FileNotFoundException { 1442 final ContentResolver resolver = getContext().getContentResolver(); 1443 Cursor cursor = null; 1444 try { 1445 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 1446 ImagesBucketThumbnailQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + bucketId, 1447 null, ImageColumns.DATE_MODIFIED + " DESC"); 1448 if (cursor.moveToFirst()) { 1449 return cursor.getLong(ImagesBucketThumbnailQuery._ID); 1450 } 1451 } finally { 1452 FileUtils.closeQuietly(cursor); 1453 } 1454 throw new FileNotFoundException("No video found for bucket"); 1455 } 1456 openOrCreateImageThumbnailCleared(long id, Point size, CancellationSignal signal)1457 private AssetFileDescriptor openOrCreateImageThumbnailCleared(long id, Point size, 1458 CancellationSignal signal) throws FileNotFoundException { 1459 final Bundle opts = new Bundle(); 1460 opts.putParcelable(EXTRA_SIZE, size); 1461 1462 final Uri uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id); 1463 return getContext().getContentResolver().openTypedAssetFile(uri, "image/*", opts, signal); 1464 } 1465 1466 private interface VideosBucketThumbnailQuery { 1467 final String[] PROJECTION = new String[] { 1468 VideoColumns._ID, 1469 VideoColumns.BUCKET_ID, 1470 VideoColumns.DATE_MODIFIED }; 1471 1472 final int _ID = 0; 1473 final int BUCKET_ID = 1; 1474 final int DATE_MODIFIED = 2; 1475 } 1476 getVideoForBucketCleared(long bucketId)1477 private long getVideoForBucketCleared(long bucketId) 1478 throws FileNotFoundException { 1479 final ContentResolver resolver = getContext().getContentResolver(); 1480 Cursor cursor = null; 1481 try { 1482 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 1483 VideosBucketThumbnailQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + bucketId, 1484 null, VideoColumns.DATE_MODIFIED + " DESC"); 1485 if (cursor.moveToFirst()) { 1486 return cursor.getLong(VideosBucketThumbnailQuery._ID); 1487 } 1488 } finally { 1489 FileUtils.closeQuietly(cursor); 1490 } 1491 throw new FileNotFoundException("No video found for bucket"); 1492 } 1493 openOrCreateVideoThumbnailCleared(long id, Point size, CancellationSignal signal)1494 private AssetFileDescriptor openOrCreateVideoThumbnailCleared(long id, Point size, 1495 CancellationSignal signal) throws FileNotFoundException { 1496 final Bundle opts = new Bundle(); 1497 opts.putParcelable(EXTRA_SIZE, size); 1498 1499 final Uri uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id); 1500 return getContext().getContentResolver().openTypedAssetFile(uri, "image/*", opts, signal); 1501 } 1502 cleanUpMediaDisplayName(String displayName)1503 private String cleanUpMediaDisplayName(String displayName) { 1504 if (!MediaStore.UNKNOWN_STRING.equals(displayName)) { 1505 return displayName; 1506 } 1507 return getContext().getResources().getString(R.string.unknown); 1508 } 1509 cleanUpMediaBucketName(String bucketDisplayName, String volumeName)1510 private String cleanUpMediaBucketName(String bucketDisplayName, String volumeName) { 1511 if (!TextUtils.isEmpty(bucketDisplayName)) { 1512 return bucketDisplayName; 1513 } else if (!Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL_PRIMARY)) { 1514 return volumeName; 1515 } else { 1516 return getContext().getResources().getString(R.string.unknown); 1517 } 1518 } 1519 } 1520