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