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