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