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