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