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