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.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.Context; 22 import android.content.res.AssetFileDescriptor; 23 import android.database.Cursor; 24 import android.database.MatrixCursor; 25 import android.database.MatrixCursor.RowBuilder; 26 import android.graphics.BitmapFactory; 27 import android.graphics.Point; 28 import android.net.Uri; 29 import android.os.Binder; 30 import android.os.Bundle; 31 import android.os.CancellationSignal; 32 import android.os.ParcelFileDescriptor; 33 import android.provider.BaseColumns; 34 import android.provider.DocumentsContract; 35 import android.provider.DocumentsContract.Document; 36 import android.provider.DocumentsContract.Root; 37 import android.provider.DocumentsProvider; 38 import android.provider.MediaStore.Audio; 39 import android.provider.MediaStore.Audio.AlbumColumns; 40 import android.provider.MediaStore.Audio.Albums; 41 import android.provider.MediaStore.Audio.ArtistColumns; 42 import android.provider.MediaStore.Audio.Artists; 43 import android.provider.MediaStore.Audio.AudioColumns; 44 import android.provider.MediaStore.Files.FileColumns; 45 import android.provider.MediaStore.Images; 46 import android.provider.MediaStore.Images.ImageColumns; 47 import android.provider.MediaStore.Video; 48 import android.provider.MediaStore.Video.VideoColumns; 49 import android.text.TextUtils; 50 import android.text.format.DateUtils; 51 import android.util.Log; 52 53 import libcore.io.IoUtils; 54 55 import java.io.File; 56 import java.io.FileNotFoundException; 57 58 /** 59 * Presents a {@link DocumentsContract} view of {@link MediaProvider} external 60 * contents. 61 */ 62 public class MediaDocumentsProvider extends DocumentsProvider { 63 private static final String TAG = "MediaDocumentsProvider"; 64 65 private static final String AUTHORITY = "com.android.providers.media.documents"; 66 67 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 68 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 69 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_MIME_TYPES 70 }; 71 72 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 73 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 74 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 75 }; 76 77 private static final String IMAGE_MIME_TYPES = joinNewline("image/*"); 78 79 private static final String VIDEO_MIME_TYPES = joinNewline("video/*"); 80 81 private static final String AUDIO_MIME_TYPES = joinNewline( 82 "audio/*", "application/ogg", "application/x-flac"); 83 84 private static final String TYPE_IMAGES_ROOT = "images_root"; 85 private static final String TYPE_IMAGES_BUCKET = "images_bucket"; 86 private static final String TYPE_IMAGE = "image"; 87 88 private static final String TYPE_VIDEOS_ROOT = "videos_root"; 89 private static final String TYPE_VIDEOS_BUCKET = "videos_bucket"; 90 private static final String TYPE_VIDEO = "video"; 91 92 private static final String TYPE_AUDIO_ROOT = "audio_root"; 93 private static final String TYPE_AUDIO = "audio"; 94 private static final String TYPE_ARTIST = "artist"; 95 private static final String TYPE_ALBUM = "album"; 96 97 private static boolean sReturnedImagesEmpty = false; 98 private static boolean sReturnedVideosEmpty = false; 99 private static boolean sReturnedAudioEmpty = false; 100 joinNewline(String... args)101 private static String joinNewline(String... args) { 102 return TextUtils.join("\n", args); 103 } 104 copyNotificationUri(MatrixCursor result, Cursor cursor)105 private void copyNotificationUri(MatrixCursor result, Cursor cursor) { 106 result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri()); 107 } 108 109 @Override onCreate()110 public boolean onCreate() { 111 return true; 112 } 113 notifyRootsChanged(Context context)114 private static void notifyRootsChanged(Context context) { 115 context.getContentResolver() 116 .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false); 117 } 118 119 /** 120 * When inserting the first item of each type, we need to trigger a roots 121 * refresh to clear a previously reported {@link Root#FLAG_EMPTY}. 122 */ onMediaStoreInsert(Context context, String volumeName, int type, long id)123 static void onMediaStoreInsert(Context context, String volumeName, int type, long id) { 124 if (!"external".equals(volumeName)) return; 125 126 if (type == FileColumns.MEDIA_TYPE_IMAGE && sReturnedImagesEmpty) { 127 sReturnedImagesEmpty = false; 128 notifyRootsChanged(context); 129 } else if (type == FileColumns.MEDIA_TYPE_VIDEO && sReturnedVideosEmpty) { 130 sReturnedVideosEmpty = false; 131 notifyRootsChanged(context); 132 } else if (type == FileColumns.MEDIA_TYPE_AUDIO && sReturnedAudioEmpty) { 133 sReturnedAudioEmpty = false; 134 notifyRootsChanged(context); 135 } 136 } 137 138 /** 139 * When deleting an item, we need to revoke any outstanding Uri grants. 140 */ onMediaStoreDelete(Context context, String volumeName, int type, long id)141 static void onMediaStoreDelete(Context context, String volumeName, int type, long id) { 142 if (!"external".equals(volumeName)) return; 143 144 if (type == FileColumns.MEDIA_TYPE_IMAGE) { 145 final Uri uri = DocumentsContract.buildDocumentUri( 146 AUTHORITY, getDocIdForIdent(TYPE_IMAGE, id)); 147 context.revokeUriPermission(uri, ~0); 148 } else if (type == FileColumns.MEDIA_TYPE_VIDEO) { 149 final Uri uri = DocumentsContract.buildDocumentUri( 150 AUTHORITY, getDocIdForIdent(TYPE_VIDEO, id)); 151 context.revokeUriPermission(uri, ~0); 152 } else if (type == FileColumns.MEDIA_TYPE_AUDIO) { 153 final Uri uri = DocumentsContract.buildDocumentUri( 154 AUTHORITY, getDocIdForIdent(TYPE_AUDIO, id)); 155 context.revokeUriPermission(uri, ~0); 156 } 157 } 158 159 private static class Ident { 160 public String type; 161 public long id; 162 } 163 getIdentForDocId(String docId)164 private static Ident getIdentForDocId(String docId) { 165 final Ident ident = new Ident(); 166 final int split = docId.indexOf(':'); 167 if (split == -1) { 168 ident.type = docId; 169 ident.id = -1; 170 } else { 171 ident.type = docId.substring(0, split); 172 ident.id = Long.parseLong(docId.substring(split + 1)); 173 } 174 return ident; 175 } 176 getDocIdForIdent(String type, long id)177 private static String getDocIdForIdent(String type, long id) { 178 return type + ":" + id; 179 } 180 resolveRootProjection(String[] projection)181 private static String[] resolveRootProjection(String[] projection) { 182 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 183 } 184 resolveDocumentProjection(String[] projection)185 private static String[] resolveDocumentProjection(String[] projection) { 186 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 187 } 188 189 @Override queryRoots(String[] projection)190 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 191 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 192 includeImagesRoot(result); 193 includeVideosRoot(result); 194 includeAudioRoot(result); 195 return result; 196 } 197 198 @Override queryDocument(String docId, String[] projection)199 public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException { 200 final ContentResolver resolver = getContext().getContentResolver(); 201 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 202 final Ident ident = getIdentForDocId(docId); 203 204 final long token = Binder.clearCallingIdentity(); 205 Cursor cursor = null; 206 try { 207 if (TYPE_IMAGES_ROOT.equals(ident.type)) { 208 // single root 209 includeImagesRootDocument(result); 210 } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) { 211 // single bucket 212 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 213 ImagesBucketQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + ident.id, 214 null, ImagesBucketQuery.SORT_ORDER); 215 copyNotificationUri(result, cursor); 216 if (cursor.moveToFirst()) { 217 includeImagesBucket(result, cursor); 218 } 219 } else if (TYPE_IMAGE.equals(ident.type)) { 220 // single image 221 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 222 ImageQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null, 223 null); 224 copyNotificationUri(result, cursor); 225 if (cursor.moveToFirst()) { 226 includeImage(result, cursor); 227 } 228 } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) { 229 // single root 230 includeVideosRootDocument(result); 231 } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { 232 // single bucket 233 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 234 VideosBucketQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + ident.id, 235 null, VideosBucketQuery.SORT_ORDER); 236 copyNotificationUri(result, cursor); 237 if (cursor.moveToFirst()) { 238 includeVideosBucket(result, cursor); 239 } 240 } else if (TYPE_VIDEO.equals(ident.type)) { 241 // single video 242 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 243 VideoQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null, 244 null); 245 copyNotificationUri(result, cursor); 246 if (cursor.moveToFirst()) { 247 includeVideo(result, cursor); 248 } 249 } else if (TYPE_AUDIO_ROOT.equals(ident.type)) { 250 // single root 251 includeAudioRootDocument(result); 252 } else if (TYPE_ARTIST.equals(ident.type)) { 253 // single artist 254 cursor = resolver.query(Artists.EXTERNAL_CONTENT_URI, 255 ArtistQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null, 256 null); 257 copyNotificationUri(result, cursor); 258 if (cursor.moveToFirst()) { 259 includeArtist(result, cursor); 260 } 261 } else if (TYPE_ALBUM.equals(ident.type)) { 262 // single album 263 cursor = resolver.query(Albums.EXTERNAL_CONTENT_URI, 264 AlbumQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null, 265 null); 266 copyNotificationUri(result, cursor); 267 if (cursor.moveToFirst()) { 268 includeAlbum(result, cursor); 269 } 270 } else if (TYPE_AUDIO.equals(ident.type)) { 271 // single song 272 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, 273 SongQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null, 274 null); 275 copyNotificationUri(result, cursor); 276 if (cursor.moveToFirst()) { 277 includeAudio(result, cursor); 278 } 279 } else { 280 throw new UnsupportedOperationException("Unsupported document " + docId); 281 } 282 } finally { 283 IoUtils.closeQuietly(cursor); 284 Binder.restoreCallingIdentity(token); 285 } 286 return result; 287 } 288 289 @Override queryChildDocuments(String docId, String[] projection, String sortOrder)290 public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder) 291 throws FileNotFoundException { 292 final ContentResolver resolver = getContext().getContentResolver(); 293 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 294 final Ident ident = getIdentForDocId(docId); 295 296 final long token = Binder.clearCallingIdentity(); 297 Cursor cursor = null; 298 try { 299 if (TYPE_IMAGES_ROOT.equals(ident.type)) { 300 // include all unique buckets 301 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 302 ImagesBucketQuery.PROJECTION, null, null, ImagesBucketQuery.SORT_ORDER); 303 // multiple orders 304 copyNotificationUri(result, cursor); 305 long lastId = Long.MIN_VALUE; 306 while (cursor.moveToNext()) { 307 final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID); 308 if (lastId != id) { 309 includeImagesBucket(result, cursor); 310 lastId = id; 311 } 312 } 313 } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) { 314 // include images under bucket 315 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 316 ImageQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + ident.id, 317 null, null); 318 copyNotificationUri(result, cursor); 319 while (cursor.moveToNext()) { 320 includeImage(result, cursor); 321 } 322 } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) { 323 // include all unique buckets 324 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 325 VideosBucketQuery.PROJECTION, null, null, VideosBucketQuery.SORT_ORDER); 326 copyNotificationUri(result, cursor); 327 long lastId = Long.MIN_VALUE; 328 while (cursor.moveToNext()) { 329 final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID); 330 if (lastId != id) { 331 includeVideosBucket(result, cursor); 332 lastId = id; 333 } 334 } 335 } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { 336 // include videos under bucket 337 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 338 VideoQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + ident.id, 339 null, null); 340 copyNotificationUri(result, cursor); 341 while (cursor.moveToNext()) { 342 includeVideo(result, cursor); 343 } 344 } else if (TYPE_AUDIO_ROOT.equals(ident.type)) { 345 // include all artists 346 cursor = resolver.query(Audio.Artists.EXTERNAL_CONTENT_URI, 347 ArtistQuery.PROJECTION, null, null, null); 348 copyNotificationUri(result, cursor); 349 while (cursor.moveToNext()) { 350 includeArtist(result, cursor); 351 } 352 } else if (TYPE_ARTIST.equals(ident.type)) { 353 // include all albums under artist 354 cursor = resolver.query(Artists.Albums.getContentUri("external", ident.id), 355 AlbumQuery.PROJECTION, null, null, null); 356 copyNotificationUri(result, cursor); 357 while (cursor.moveToNext()) { 358 includeAlbum(result, cursor); 359 } 360 } else if (TYPE_ALBUM.equals(ident.type)) { 361 // include all songs under album 362 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, 363 SongQuery.PROJECTION, AudioColumns.ALBUM_ID + "=" + ident.id, 364 null, null); 365 copyNotificationUri(result, cursor); 366 while (cursor.moveToNext()) { 367 includeAudio(result, cursor); 368 } 369 } else { 370 throw new UnsupportedOperationException("Unsupported document " + docId); 371 } 372 } finally { 373 IoUtils.closeQuietly(cursor); 374 Binder.restoreCallingIdentity(token); 375 } 376 return result; 377 } 378 379 @Override queryRecentDocuments(String rootId, String[] projection)380 public Cursor queryRecentDocuments(String rootId, String[] projection) 381 throws FileNotFoundException { 382 final ContentResolver resolver = getContext().getContentResolver(); 383 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 384 385 final long token = Binder.clearCallingIdentity(); 386 Cursor cursor = null; 387 try { 388 if (TYPE_IMAGES_ROOT.equals(rootId)) { 389 // include all unique buckets 390 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 391 ImageQuery.PROJECTION, null, null, ImageColumns.DATE_MODIFIED + " DESC"); 392 copyNotificationUri(result, cursor); 393 while (cursor.moveToNext() && result.getCount() < 64) { 394 includeImage(result, cursor); 395 } 396 } else if (TYPE_VIDEOS_ROOT.equals(rootId)) { 397 // include all unique buckets 398 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 399 VideoQuery.PROJECTION, null, null, VideoColumns.DATE_MODIFIED + " DESC"); 400 copyNotificationUri(result, cursor); 401 while (cursor.moveToNext() && result.getCount() < 64) { 402 includeVideo(result, cursor); 403 } 404 } else { 405 throw new UnsupportedOperationException("Unsupported root " + rootId); 406 } 407 } finally { 408 IoUtils.closeQuietly(cursor); 409 Binder.restoreCallingIdentity(token); 410 } 411 return result; 412 } 413 414 @Override openDocument(String docId, String mode, CancellationSignal signal)415 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) 416 throws FileNotFoundException { 417 final Ident ident = getIdentForDocId(docId); 418 419 if (!"r".equals(mode)) { 420 throw new IllegalArgumentException("Media is read-only"); 421 } 422 423 final Uri target; 424 if (TYPE_IMAGE.equals(ident.type) && ident.id != -1) { 425 target = ContentUris.withAppendedId( 426 Images.Media.EXTERNAL_CONTENT_URI, ident.id); 427 } else if (TYPE_VIDEO.equals(ident.type) && ident.id != -1) { 428 target = ContentUris.withAppendedId( 429 Video.Media.EXTERNAL_CONTENT_URI, ident.id); 430 } else if (TYPE_AUDIO.equals(ident.type) && ident.id != -1) { 431 target = ContentUris.withAppendedId( 432 Audio.Media.EXTERNAL_CONTENT_URI, ident.id); 433 } else { 434 throw new UnsupportedOperationException("Unsupported document " + docId); 435 } 436 437 // Delegate to real provider 438 final long token = Binder.clearCallingIdentity(); 439 try { 440 return getContext().getContentResolver().openFileDescriptor(target, mode); 441 } finally { 442 Binder.restoreCallingIdentity(token); 443 } 444 } 445 446 @Override openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)447 public AssetFileDescriptor openDocumentThumbnail( 448 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 449 final ContentResolver resolver = getContext().getContentResolver(); 450 final Ident ident = getIdentForDocId(docId); 451 452 final long token = Binder.clearCallingIdentity(); 453 try { 454 if (TYPE_IMAGES_BUCKET.equals(ident.type)) { 455 final long id = getImageForBucketCleared(ident.id); 456 return openOrCreateImageThumbnailCleared(id, signal); 457 } else if (TYPE_IMAGE.equals(ident.type)) { 458 return openOrCreateImageThumbnailCleared(ident.id, signal); 459 } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { 460 final long id = getVideoForBucketCleared(ident.id); 461 return openOrCreateVideoThumbnailCleared(id, signal); 462 } else if (TYPE_VIDEO.equals(ident.type)) { 463 return openOrCreateVideoThumbnailCleared(ident.id, signal); 464 } else { 465 throw new UnsupportedOperationException("Unsupported document " + docId); 466 } 467 } finally { 468 Binder.restoreCallingIdentity(token); 469 } 470 } 471 isEmpty(Uri uri)472 private boolean isEmpty(Uri uri) { 473 final ContentResolver resolver = getContext().getContentResolver(); 474 final long token = Binder.clearCallingIdentity(); 475 Cursor cursor = null; 476 try { 477 cursor = resolver.query(uri, new String[] { 478 BaseColumns._ID }, null, null, null); 479 return (cursor == null) || (cursor.getCount() == 0); 480 } finally { 481 IoUtils.closeQuietly(cursor); 482 Binder.restoreCallingIdentity(token); 483 } 484 } 485 includeImagesRoot(MatrixCursor result)486 private void includeImagesRoot(MatrixCursor result) { 487 int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS; 488 if (isEmpty(Images.Media.EXTERNAL_CONTENT_URI)) { 489 flags |= Root.FLAG_EMPTY; 490 sReturnedImagesEmpty = true; 491 } 492 493 final RowBuilder row = result.newRow(); 494 row.add(Root.COLUMN_ROOT_ID, TYPE_IMAGES_ROOT); 495 row.add(Root.COLUMN_FLAGS, flags); 496 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_images)); 497 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT); 498 row.add(Root.COLUMN_MIME_TYPES, IMAGE_MIME_TYPES); 499 } 500 includeVideosRoot(MatrixCursor result)501 private void includeVideosRoot(MatrixCursor result) { 502 int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS; 503 if (isEmpty(Video.Media.EXTERNAL_CONTENT_URI)) { 504 flags |= Root.FLAG_EMPTY; 505 sReturnedVideosEmpty = true; 506 } 507 508 final RowBuilder row = result.newRow(); 509 row.add(Root.COLUMN_ROOT_ID, TYPE_VIDEOS_ROOT); 510 row.add(Root.COLUMN_FLAGS, flags); 511 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_videos)); 512 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT); 513 row.add(Root.COLUMN_MIME_TYPES, VIDEO_MIME_TYPES); 514 } 515 includeAudioRoot(MatrixCursor result)516 private void includeAudioRoot(MatrixCursor result) { 517 int flags = Root.FLAG_LOCAL_ONLY; 518 if (isEmpty(Audio.Media.EXTERNAL_CONTENT_URI)) { 519 flags |= Root.FLAG_EMPTY; 520 sReturnedAudioEmpty = true; 521 } 522 523 final RowBuilder row = result.newRow(); 524 row.add(Root.COLUMN_ROOT_ID, TYPE_AUDIO_ROOT); 525 row.add(Root.COLUMN_FLAGS, flags); 526 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_audio)); 527 row.add(Root.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT); 528 row.add(Root.COLUMN_MIME_TYPES, AUDIO_MIME_TYPES); 529 } 530 includeImagesRootDocument(MatrixCursor result)531 private void includeImagesRootDocument(MatrixCursor result) { 532 final RowBuilder row = result.newRow(); 533 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT); 534 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_images)); 535 row.add(Document.COLUMN_FLAGS, 536 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 537 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 538 } 539 includeVideosRootDocument(MatrixCursor result)540 private void includeVideosRootDocument(MatrixCursor result) { 541 final RowBuilder row = result.newRow(); 542 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT); 543 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_videos)); 544 row.add(Document.COLUMN_FLAGS, 545 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); 546 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 547 } 548 includeAudioRootDocument(MatrixCursor result)549 private void includeAudioRootDocument(MatrixCursor result) { 550 final RowBuilder row = result.newRow(); 551 row.add(Document.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT); 552 row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_audio)); 553 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 554 } 555 556 private interface ImagesBucketQuery { 557 final String[] PROJECTION = new String[] { 558 ImageColumns.BUCKET_ID, 559 ImageColumns.BUCKET_DISPLAY_NAME, 560 ImageColumns.DATE_MODIFIED }; 561 final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED 562 + " DESC"; 563 564 final int BUCKET_ID = 0; 565 final int BUCKET_DISPLAY_NAME = 1; 566 final int DATE_MODIFIED = 2; 567 } 568 includeImagesBucket(MatrixCursor result, Cursor cursor)569 private void includeImagesBucket(MatrixCursor result, Cursor cursor) { 570 final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID); 571 final String docId = getDocIdForIdent(TYPE_IMAGES_BUCKET, id); 572 573 final RowBuilder row = result.newRow(); 574 row.add(Document.COLUMN_DOCUMENT_ID, docId); 575 row.add(Document.COLUMN_DISPLAY_NAME, 576 cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME)); 577 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 578 row.add(Document.COLUMN_LAST_MODIFIED, 579 cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 580 row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID 581 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED 582 | Document.FLAG_DIR_HIDE_GRID_TITLES); 583 } 584 585 private interface ImageQuery { 586 final String[] PROJECTION = new String[] { 587 ImageColumns._ID, 588 ImageColumns.DISPLAY_NAME, 589 ImageColumns.MIME_TYPE, 590 ImageColumns.SIZE, 591 ImageColumns.DATE_MODIFIED }; 592 593 final int _ID = 0; 594 final int DISPLAY_NAME = 1; 595 final int MIME_TYPE = 2; 596 final int SIZE = 3; 597 final int DATE_MODIFIED = 4; 598 } 599 includeImage(MatrixCursor result, Cursor cursor)600 private void includeImage(MatrixCursor result, Cursor cursor) { 601 final long id = cursor.getLong(ImageQuery._ID); 602 final String docId = getDocIdForIdent(TYPE_IMAGE, id); 603 604 final RowBuilder row = result.newRow(); 605 row.add(Document.COLUMN_DOCUMENT_ID, docId); 606 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ImageQuery.DISPLAY_NAME)); 607 row.add(Document.COLUMN_SIZE, cursor.getLong(ImageQuery.SIZE)); 608 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(ImageQuery.MIME_TYPE)); 609 row.add(Document.COLUMN_LAST_MODIFIED, 610 cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 611 row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_THUMBNAIL); 612 } 613 614 private interface VideosBucketQuery { 615 final String[] PROJECTION = new String[] { 616 VideoColumns.BUCKET_ID, 617 VideoColumns.BUCKET_DISPLAY_NAME, 618 VideoColumns.DATE_MODIFIED }; 619 final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED 620 + " DESC"; 621 622 final int BUCKET_ID = 0; 623 final int BUCKET_DISPLAY_NAME = 1; 624 final int DATE_MODIFIED = 2; 625 } 626 includeVideosBucket(MatrixCursor result, Cursor cursor)627 private void includeVideosBucket(MatrixCursor result, Cursor cursor) { 628 final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID); 629 final String docId = getDocIdForIdent(TYPE_VIDEOS_BUCKET, id); 630 631 final RowBuilder row = result.newRow(); 632 row.add(Document.COLUMN_DOCUMENT_ID, docId); 633 row.add(Document.COLUMN_DISPLAY_NAME, 634 cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME)); 635 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 636 row.add(Document.COLUMN_LAST_MODIFIED, 637 cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 638 row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID 639 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED 640 | Document.FLAG_DIR_HIDE_GRID_TITLES); 641 } 642 643 private interface VideoQuery { 644 final String[] PROJECTION = new String[] { 645 VideoColumns._ID, 646 VideoColumns.DISPLAY_NAME, 647 VideoColumns.MIME_TYPE, 648 VideoColumns.SIZE, 649 VideoColumns.DATE_MODIFIED }; 650 651 final int _ID = 0; 652 final int DISPLAY_NAME = 1; 653 final int MIME_TYPE = 2; 654 final int SIZE = 3; 655 final int DATE_MODIFIED = 4; 656 } 657 includeVideo(MatrixCursor result, Cursor cursor)658 private void includeVideo(MatrixCursor result, Cursor cursor) { 659 final long id = cursor.getLong(VideoQuery._ID); 660 final String docId = getDocIdForIdent(TYPE_VIDEO, id); 661 662 final RowBuilder row = result.newRow(); 663 row.add(Document.COLUMN_DOCUMENT_ID, docId); 664 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(VideoQuery.DISPLAY_NAME)); 665 row.add(Document.COLUMN_SIZE, cursor.getLong(VideoQuery.SIZE)); 666 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(VideoQuery.MIME_TYPE)); 667 row.add(Document.COLUMN_LAST_MODIFIED, 668 cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 669 row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_THUMBNAIL); 670 } 671 672 private interface ArtistQuery { 673 final String[] PROJECTION = new String[] { 674 BaseColumns._ID, 675 ArtistColumns.ARTIST }; 676 677 final int _ID = 0; 678 final int ARTIST = 1; 679 } 680 includeArtist(MatrixCursor result, Cursor cursor)681 private void includeArtist(MatrixCursor result, Cursor cursor) { 682 final long id = cursor.getLong(ArtistQuery._ID); 683 final String docId = getDocIdForIdent(TYPE_ARTIST, id); 684 685 final RowBuilder row = result.newRow(); 686 row.add(Document.COLUMN_DOCUMENT_ID, docId); 687 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ArtistQuery.ARTIST)); 688 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 689 } 690 691 private interface AlbumQuery { 692 final String[] PROJECTION = new String[] { 693 BaseColumns._ID, 694 AlbumColumns.ALBUM }; 695 696 final int _ID = 0; 697 final int ALBUM = 1; 698 } 699 includeAlbum(MatrixCursor result, Cursor cursor)700 private void includeAlbum(MatrixCursor result, Cursor cursor) { 701 final long id = cursor.getLong(AlbumQuery._ID); 702 final String docId = getDocIdForIdent(TYPE_ALBUM, id); 703 704 final RowBuilder row = result.newRow(); 705 row.add(Document.COLUMN_DOCUMENT_ID, docId); 706 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(AlbumQuery.ALBUM)); 707 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 708 } 709 710 private interface SongQuery { 711 final String[] PROJECTION = new String[] { 712 AudioColumns._ID, 713 AudioColumns.TITLE, 714 AudioColumns.MIME_TYPE, 715 AudioColumns.SIZE, 716 AudioColumns.DATE_MODIFIED }; 717 718 final int _ID = 0; 719 final int TITLE = 1; 720 final int MIME_TYPE = 2; 721 final int SIZE = 3; 722 final int DATE_MODIFIED = 4; 723 } 724 includeAudio(MatrixCursor result, Cursor cursor)725 private void includeAudio(MatrixCursor result, Cursor cursor) { 726 final long id = cursor.getLong(SongQuery._ID); 727 final String docId = getDocIdForIdent(TYPE_AUDIO, id); 728 729 final RowBuilder row = result.newRow(); 730 row.add(Document.COLUMN_DOCUMENT_ID, docId); 731 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(SongQuery.TITLE)); 732 row.add(Document.COLUMN_SIZE, cursor.getLong(SongQuery.SIZE)); 733 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE)); 734 row.add(Document.COLUMN_LAST_MODIFIED, 735 cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 736 } 737 738 private interface ImagesBucketThumbnailQuery { 739 final String[] PROJECTION = new String[] { 740 ImageColumns._ID, 741 ImageColumns.BUCKET_ID, 742 ImageColumns.DATE_MODIFIED }; 743 744 final int _ID = 0; 745 final int BUCKET_ID = 1; 746 final int DATE_MODIFIED = 2; 747 } 748 getImageForBucketCleared(long bucketId)749 private long getImageForBucketCleared(long bucketId) throws FileNotFoundException { 750 final ContentResolver resolver = getContext().getContentResolver(); 751 Cursor cursor = null; 752 try { 753 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 754 ImagesBucketThumbnailQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + bucketId, 755 null, ImageColumns.DATE_MODIFIED + " DESC"); 756 if (cursor.moveToFirst()) { 757 return cursor.getLong(ImagesBucketThumbnailQuery._ID); 758 } 759 } finally { 760 IoUtils.closeQuietly(cursor); 761 } 762 throw new FileNotFoundException("No video found for bucket"); 763 } 764 765 private interface ImageThumbnailQuery { 766 final String[] PROJECTION = new String[] { 767 Images.Thumbnails.DATA }; 768 769 final int _DATA = 0; 770 } 771 openImageThumbnailCleared(long id, CancellationSignal signal)772 private ParcelFileDescriptor openImageThumbnailCleared(long id, CancellationSignal signal) 773 throws FileNotFoundException { 774 final ContentResolver resolver = getContext().getContentResolver(); 775 776 Cursor cursor = null; 777 try { 778 cursor = resolver.query(Images.Thumbnails.EXTERNAL_CONTENT_URI, 779 ImageThumbnailQuery.PROJECTION, Images.Thumbnails.IMAGE_ID + "=" + id, null, 780 null, signal); 781 if (cursor.moveToFirst()) { 782 final String data = cursor.getString(ImageThumbnailQuery._DATA); 783 return ParcelFileDescriptor.open( 784 new File(data), ParcelFileDescriptor.MODE_READ_ONLY); 785 } 786 } finally { 787 IoUtils.closeQuietly(cursor); 788 } 789 return null; 790 } 791 openOrCreateImageThumbnailCleared( long id, CancellationSignal signal)792 private AssetFileDescriptor openOrCreateImageThumbnailCleared( 793 long id, CancellationSignal signal) throws FileNotFoundException { 794 final ContentResolver resolver = getContext().getContentResolver(); 795 796 ParcelFileDescriptor pfd = openImageThumbnailCleared(id, signal); 797 if (pfd == null) { 798 // No thumbnail yet, so generate. This is messy, since we drop the 799 // Bitmap on the floor, but its the least-complicated way. 800 final BitmapFactory.Options opts = new BitmapFactory.Options(); 801 opts.inJustDecodeBounds = true; 802 Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, opts); 803 804 pfd = openImageThumbnailCleared(id, signal); 805 } 806 807 if (pfd == null) { 808 // Phoey, fallback to full image 809 final Uri fullUri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id); 810 pfd = resolver.openFileDescriptor(fullUri, "r", signal); 811 } 812 813 final int orientation = queryOrientationForImage(id, signal); 814 final Bundle extras; 815 if (orientation != 0) { 816 extras = new Bundle(1); 817 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, orientation); 818 } else { 819 extras = null; 820 } 821 822 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, extras); 823 } 824 825 private interface VideosBucketThumbnailQuery { 826 final String[] PROJECTION = new String[] { 827 VideoColumns._ID, 828 VideoColumns.BUCKET_ID, 829 VideoColumns.DATE_MODIFIED }; 830 831 final int _ID = 0; 832 final int BUCKET_ID = 1; 833 final int DATE_MODIFIED = 2; 834 } 835 getVideoForBucketCleared(long bucketId)836 private long getVideoForBucketCleared(long bucketId) 837 throws FileNotFoundException { 838 final ContentResolver resolver = getContext().getContentResolver(); 839 Cursor cursor = null; 840 try { 841 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 842 VideosBucketThumbnailQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + bucketId, 843 null, VideoColumns.DATE_MODIFIED + " DESC"); 844 if (cursor.moveToFirst()) { 845 return cursor.getLong(VideosBucketThumbnailQuery._ID); 846 } 847 } finally { 848 IoUtils.closeQuietly(cursor); 849 } 850 throw new FileNotFoundException("No video found for bucket"); 851 } 852 853 private interface VideoThumbnailQuery { 854 final String[] PROJECTION = new String[] { 855 Video.Thumbnails.DATA }; 856 857 final int _DATA = 0; 858 } 859 openVideoThumbnailCleared(long id, CancellationSignal signal)860 private AssetFileDescriptor openVideoThumbnailCleared(long id, CancellationSignal signal) 861 throws FileNotFoundException { 862 final ContentResolver resolver = getContext().getContentResolver(); 863 Cursor cursor = null; 864 try { 865 cursor = resolver.query(Video.Thumbnails.EXTERNAL_CONTENT_URI, 866 VideoThumbnailQuery.PROJECTION, Video.Thumbnails.VIDEO_ID + "=" + id, null, 867 null, signal); 868 if (cursor.moveToFirst()) { 869 final String data = cursor.getString(VideoThumbnailQuery._DATA); 870 return new AssetFileDescriptor(ParcelFileDescriptor.open( 871 new File(data), ParcelFileDescriptor.MODE_READ_ONLY), 0, 872 AssetFileDescriptor.UNKNOWN_LENGTH); 873 } 874 } finally { 875 IoUtils.closeQuietly(cursor); 876 } 877 return null; 878 } 879 openOrCreateVideoThumbnailCleared( long id, CancellationSignal signal)880 private AssetFileDescriptor openOrCreateVideoThumbnailCleared( 881 long id, CancellationSignal signal) throws FileNotFoundException { 882 final ContentResolver resolver = getContext().getContentResolver(); 883 884 AssetFileDescriptor afd = openVideoThumbnailCleared(id, signal); 885 if (afd == null) { 886 // No thumbnail yet, so generate. This is messy, since we drop the 887 // Bitmap on the floor, but its the least-complicated way. 888 final BitmapFactory.Options opts = new BitmapFactory.Options(); 889 opts.inJustDecodeBounds = true; 890 Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, opts); 891 892 afd = openVideoThumbnailCleared(id, signal); 893 } 894 895 return afd; 896 } 897 898 private interface ImageOrientationQuery { 899 final String[] PROJECTION = new String[] { 900 ImageColumns.ORIENTATION }; 901 902 final int ORIENTATION = 0; 903 } 904 queryOrientationForImage(long id, CancellationSignal signal)905 private int queryOrientationForImage(long id, CancellationSignal signal) { 906 final ContentResolver resolver = getContext().getContentResolver(); 907 908 Cursor cursor = null; 909 try { 910 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 911 ImageOrientationQuery.PROJECTION, ImageColumns._ID + "=" + id, null, null, 912 signal); 913 if (cursor.moveToFirst()) { 914 return cursor.getInt(ImageOrientationQuery.ORIENTATION); 915 } else { 916 Log.w(TAG, "Missing orientation data for " + id); 917 return 0; 918 } 919 } finally { 920 IoUtils.closeQuietly(cursor); 921 } 922 } 923 } 924