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