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