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 | Document.FLAG_DIR_HIDE_GRID_TITLES); 599 } 600 601 private interface ImageQuery { 602 final String[] PROJECTION = new String[] { 603 ImageColumns._ID, 604 ImageColumns.DISPLAY_NAME, 605 ImageColumns.MIME_TYPE, 606 ImageColumns.SIZE, 607 ImageColumns.DATE_MODIFIED }; 608 609 final int _ID = 0; 610 final int DISPLAY_NAME = 1; 611 final int MIME_TYPE = 2; 612 final int SIZE = 3; 613 final int DATE_MODIFIED = 4; 614 } 615 includeImage(MatrixCursor result, Cursor cursor)616 private void includeImage(MatrixCursor result, Cursor cursor) { 617 final long id = cursor.getLong(ImageQuery._ID); 618 final String docId = getDocIdForIdent(TYPE_IMAGE, id); 619 620 final RowBuilder row = result.newRow(); 621 row.add(Document.COLUMN_DOCUMENT_ID, docId); 622 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ImageQuery.DISPLAY_NAME)); 623 row.add(Document.COLUMN_SIZE, cursor.getLong(ImageQuery.SIZE)); 624 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(ImageQuery.MIME_TYPE)); 625 row.add(Document.COLUMN_LAST_MODIFIED, 626 cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 627 row.add(Document.COLUMN_FLAGS, 628 Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_SUPPORTS_DELETE); 629 } 630 631 private interface VideosBucketQuery { 632 final String[] PROJECTION = new String[] { 633 VideoColumns.BUCKET_ID, 634 VideoColumns.BUCKET_DISPLAY_NAME, 635 VideoColumns.DATE_MODIFIED }; 636 final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED 637 + " DESC"; 638 639 final int BUCKET_ID = 0; 640 final int BUCKET_DISPLAY_NAME = 1; 641 final int DATE_MODIFIED = 2; 642 } 643 includeVideosBucket(MatrixCursor result, Cursor cursor)644 private void includeVideosBucket(MatrixCursor result, Cursor cursor) { 645 final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID); 646 final String docId = getDocIdForIdent(TYPE_VIDEOS_BUCKET, id); 647 648 final RowBuilder row = result.newRow(); 649 row.add(Document.COLUMN_DOCUMENT_ID, docId); 650 row.add(Document.COLUMN_DISPLAY_NAME, 651 cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME)); 652 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 653 row.add(Document.COLUMN_LAST_MODIFIED, 654 cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 655 row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID 656 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED 657 | Document.FLAG_DIR_HIDE_GRID_TITLES); 658 } 659 660 private interface VideoQuery { 661 final String[] PROJECTION = new String[] { 662 VideoColumns._ID, 663 VideoColumns.DISPLAY_NAME, 664 VideoColumns.MIME_TYPE, 665 VideoColumns.SIZE, 666 VideoColumns.DATE_MODIFIED }; 667 668 final int _ID = 0; 669 final int DISPLAY_NAME = 1; 670 final int MIME_TYPE = 2; 671 final int SIZE = 3; 672 final int DATE_MODIFIED = 4; 673 } 674 includeVideo(MatrixCursor result, Cursor cursor)675 private void includeVideo(MatrixCursor result, Cursor cursor) { 676 final long id = cursor.getLong(VideoQuery._ID); 677 final String docId = getDocIdForIdent(TYPE_VIDEO, id); 678 679 final RowBuilder row = result.newRow(); 680 row.add(Document.COLUMN_DOCUMENT_ID, docId); 681 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(VideoQuery.DISPLAY_NAME)); 682 row.add(Document.COLUMN_SIZE, cursor.getLong(VideoQuery.SIZE)); 683 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(VideoQuery.MIME_TYPE)); 684 row.add(Document.COLUMN_LAST_MODIFIED, 685 cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 686 row.add(Document.COLUMN_FLAGS, 687 Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_SUPPORTS_DELETE); 688 } 689 690 private interface ArtistQuery { 691 final String[] PROJECTION = new String[] { 692 BaseColumns._ID, 693 ArtistColumns.ARTIST }; 694 695 final int _ID = 0; 696 final int ARTIST = 1; 697 } 698 includeArtist(MatrixCursor result, Cursor cursor)699 private void includeArtist(MatrixCursor result, Cursor cursor) { 700 final long id = cursor.getLong(ArtistQuery._ID); 701 final String docId = getDocIdForIdent(TYPE_ARTIST, id); 702 703 final RowBuilder row = result.newRow(); 704 row.add(Document.COLUMN_DOCUMENT_ID, docId); 705 row.add(Document.COLUMN_DISPLAY_NAME, 706 cleanUpMediaDisplayName(cursor.getString(ArtistQuery.ARTIST))); 707 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 708 } 709 710 private interface AlbumQuery { 711 final String[] PROJECTION = new String[] { 712 BaseColumns._ID, 713 AlbumColumns.ALBUM }; 714 715 final int _ID = 0; 716 final int ALBUM = 1; 717 } 718 includeAlbum(MatrixCursor result, Cursor cursor)719 private void includeAlbum(MatrixCursor result, Cursor cursor) { 720 final long id = cursor.getLong(AlbumQuery._ID); 721 final String docId = getDocIdForIdent(TYPE_ALBUM, id); 722 723 final RowBuilder row = result.newRow(); 724 row.add(Document.COLUMN_DOCUMENT_ID, docId); 725 row.add(Document.COLUMN_DISPLAY_NAME, 726 cleanUpMediaDisplayName(cursor.getString(AlbumQuery.ALBUM))); 727 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 728 } 729 730 private interface SongQuery { 731 final String[] PROJECTION = new String[] { 732 AudioColumns._ID, 733 AudioColumns.TITLE, 734 AudioColumns.MIME_TYPE, 735 AudioColumns.SIZE, 736 AudioColumns.DATE_MODIFIED }; 737 738 final int _ID = 0; 739 final int TITLE = 1; 740 final int MIME_TYPE = 2; 741 final int SIZE = 3; 742 final int DATE_MODIFIED = 4; 743 } 744 includeAudio(MatrixCursor result, Cursor cursor)745 private void includeAudio(MatrixCursor result, Cursor cursor) { 746 final long id = cursor.getLong(SongQuery._ID); 747 final String docId = getDocIdForIdent(TYPE_AUDIO, id); 748 749 final RowBuilder row = result.newRow(); 750 row.add(Document.COLUMN_DOCUMENT_ID, docId); 751 row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(SongQuery.TITLE)); 752 row.add(Document.COLUMN_SIZE, cursor.getLong(SongQuery.SIZE)); 753 row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE)); 754 row.add(Document.COLUMN_LAST_MODIFIED, 755 cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); 756 row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE); 757 } 758 759 private interface ImagesBucketThumbnailQuery { 760 final String[] PROJECTION = new String[] { 761 ImageColumns._ID, 762 ImageColumns.BUCKET_ID, 763 ImageColumns.DATE_MODIFIED }; 764 765 final int _ID = 0; 766 final int BUCKET_ID = 1; 767 final int DATE_MODIFIED = 2; 768 } 769 getImageForBucketCleared(long bucketId)770 private long getImageForBucketCleared(long bucketId) throws FileNotFoundException { 771 final ContentResolver resolver = getContext().getContentResolver(); 772 Cursor cursor = null; 773 try { 774 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 775 ImagesBucketThumbnailQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + bucketId, 776 null, ImageColumns.DATE_MODIFIED + " DESC"); 777 if (cursor.moveToFirst()) { 778 return cursor.getLong(ImagesBucketThumbnailQuery._ID); 779 } 780 } finally { 781 IoUtils.closeQuietly(cursor); 782 } 783 throw new FileNotFoundException("No video found for bucket"); 784 } 785 786 private interface ImageThumbnailQuery { 787 final String[] PROJECTION = new String[] { 788 Images.Thumbnails.DATA }; 789 790 final int _DATA = 0; 791 } 792 openImageThumbnailCleared(long id, CancellationSignal signal)793 private ParcelFileDescriptor openImageThumbnailCleared(long id, CancellationSignal signal) 794 throws FileNotFoundException { 795 final ContentResolver resolver = getContext().getContentResolver(); 796 797 Cursor cursor = null; 798 try { 799 cursor = resolver.query(Images.Thumbnails.EXTERNAL_CONTENT_URI, 800 ImageThumbnailQuery.PROJECTION, Images.Thumbnails.IMAGE_ID + "=" + id, null, 801 null, signal); 802 if (cursor.moveToFirst()) { 803 final String data = cursor.getString(ImageThumbnailQuery._DATA); 804 return ParcelFileDescriptor.open( 805 new File(data), ParcelFileDescriptor.MODE_READ_ONLY); 806 } 807 } finally { 808 IoUtils.closeQuietly(cursor); 809 } 810 return null; 811 } 812 openOrCreateImageThumbnailCleared( long id, CancellationSignal signal)813 private AssetFileDescriptor openOrCreateImageThumbnailCleared( 814 long id, CancellationSignal signal) throws FileNotFoundException { 815 final ContentResolver resolver = getContext().getContentResolver(); 816 817 ParcelFileDescriptor pfd = openImageThumbnailCleared(id, signal); 818 if (pfd == null) { 819 // No thumbnail yet, so generate. This is messy, since we drop the 820 // Bitmap on the floor, but its the least-complicated way. 821 final BitmapFactory.Options opts = new BitmapFactory.Options(); 822 opts.inJustDecodeBounds = true; 823 Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, opts); 824 825 pfd = openImageThumbnailCleared(id, signal); 826 } 827 828 if (pfd == null) { 829 // Phoey, fallback to full image 830 final Uri fullUri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id); 831 pfd = resolver.openFileDescriptor(fullUri, "r", signal); 832 } 833 834 final int orientation = queryOrientationForImage(id, signal); 835 final Bundle extras; 836 if (orientation != 0) { 837 extras = new Bundle(1); 838 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, orientation); 839 } else { 840 extras = null; 841 } 842 843 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, extras); 844 } 845 846 private interface VideosBucketThumbnailQuery { 847 final String[] PROJECTION = new String[] { 848 VideoColumns._ID, 849 VideoColumns.BUCKET_ID, 850 VideoColumns.DATE_MODIFIED }; 851 852 final int _ID = 0; 853 final int BUCKET_ID = 1; 854 final int DATE_MODIFIED = 2; 855 } 856 getVideoForBucketCleared(long bucketId)857 private long getVideoForBucketCleared(long bucketId) 858 throws FileNotFoundException { 859 final ContentResolver resolver = getContext().getContentResolver(); 860 Cursor cursor = null; 861 try { 862 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, 863 VideosBucketThumbnailQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + bucketId, 864 null, VideoColumns.DATE_MODIFIED + " DESC"); 865 if (cursor.moveToFirst()) { 866 return cursor.getLong(VideosBucketThumbnailQuery._ID); 867 } 868 } finally { 869 IoUtils.closeQuietly(cursor); 870 } 871 throw new FileNotFoundException("No video found for bucket"); 872 } 873 874 private interface VideoThumbnailQuery { 875 final String[] PROJECTION = new String[] { 876 Video.Thumbnails.DATA }; 877 878 final int _DATA = 0; 879 } 880 openVideoThumbnailCleared(long id, CancellationSignal signal)881 private AssetFileDescriptor openVideoThumbnailCleared(long id, CancellationSignal signal) 882 throws FileNotFoundException { 883 final ContentResolver resolver = getContext().getContentResolver(); 884 Cursor cursor = null; 885 try { 886 cursor = resolver.query(Video.Thumbnails.EXTERNAL_CONTENT_URI, 887 VideoThumbnailQuery.PROJECTION, Video.Thumbnails.VIDEO_ID + "=" + id, null, 888 null, signal); 889 if (cursor.moveToFirst()) { 890 final String data = cursor.getString(VideoThumbnailQuery._DATA); 891 return new AssetFileDescriptor(ParcelFileDescriptor.open( 892 new File(data), ParcelFileDescriptor.MODE_READ_ONLY), 0, 893 AssetFileDescriptor.UNKNOWN_LENGTH); 894 } 895 } finally { 896 IoUtils.closeQuietly(cursor); 897 } 898 return null; 899 } 900 openOrCreateVideoThumbnailCleared( long id, CancellationSignal signal)901 private AssetFileDescriptor openOrCreateVideoThumbnailCleared( 902 long id, CancellationSignal signal) throws FileNotFoundException { 903 final ContentResolver resolver = getContext().getContentResolver(); 904 905 AssetFileDescriptor afd = openVideoThumbnailCleared(id, signal); 906 if (afd == null) { 907 // No thumbnail yet, so generate. This is messy, since we drop the 908 // Bitmap on the floor, but its the least-complicated way. 909 final BitmapFactory.Options opts = new BitmapFactory.Options(); 910 opts.inJustDecodeBounds = true; 911 Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, opts); 912 913 afd = openVideoThumbnailCleared(id, signal); 914 } 915 916 return afd; 917 } 918 919 private interface ImageOrientationQuery { 920 final String[] PROJECTION = new String[] { 921 ImageColumns.ORIENTATION }; 922 923 final int ORIENTATION = 0; 924 } 925 queryOrientationForImage(long id, CancellationSignal signal)926 private int queryOrientationForImage(long id, CancellationSignal signal) { 927 final ContentResolver resolver = getContext().getContentResolver(); 928 929 Cursor cursor = null; 930 try { 931 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, 932 ImageOrientationQuery.PROJECTION, ImageColumns._ID + "=" + id, null, null, 933 signal); 934 if (cursor.moveToFirst()) { 935 return cursor.getInt(ImageOrientationQuery.ORIENTATION); 936 } else { 937 Log.w(TAG, "Missing orientation data for " + id); 938 return 0; 939 } 940 } finally { 941 IoUtils.closeQuietly(cursor); 942 } 943 } 944 cleanUpMediaDisplayName(String displayName)945 private String cleanUpMediaDisplayName(String displayName) { 946 if (!MediaStore.UNKNOWN_STRING.equals(displayName)) { 947 return displayName; 948 } 949 return getContext().getResources().getString(com.android.internal.R.string.unknownName); 950 } 951 } 952