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.annotation.Nullable;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.Context;
     23 import android.content.res.AssetFileDescriptor;
     24 import android.database.Cursor;
     25 import android.database.MatrixCursor;
     26 import android.database.MatrixCursor.RowBuilder;
     27 import android.graphics.BitmapFactory;
     28 import android.graphics.Point;
     29 import android.media.ExifInterface;
     30 import android.media.MediaMetadata;
     31 import android.net.Uri;
     32 import android.os.Binder;
     33 import android.os.Bundle;
     34 import android.os.CancellationSignal;
     35 import android.os.IBinder;
     36 import android.os.ParcelFileDescriptor;
     37 import android.os.UserHandle;
     38 import android.os.UserManager;
     39 import android.provider.BaseColumns;
     40 import android.provider.DocumentsContract;
     41 import android.provider.DocumentsContract.Document;
     42 import android.provider.DocumentsContract.Root;
     43 import android.provider.DocumentsProvider;
     44 import android.provider.MediaStore;
     45 import android.provider.MediaStore.Audio;
     46 import android.provider.MediaStore.Audio.AlbumColumns;
     47 import android.provider.MediaStore.Audio.Albums;
     48 import android.provider.MediaStore.Audio.ArtistColumns;
     49 import android.provider.MediaStore.Audio.Artists;
     50 import android.provider.MediaStore.Audio.AudioColumns;
     51 import android.provider.MediaStore.Files.FileColumns;
     52 import android.provider.MediaStore.Images;
     53 import android.provider.MediaStore.Images.ImageColumns;
     54 import android.provider.MediaStore.Video;
     55 import android.provider.MediaStore.Video.VideoColumns;
     56 import android.provider.MetadataReader;
     57 import android.text.TextUtils;
     58 import android.text.format.DateFormat;
     59 import android.text.format.DateUtils;
     60 import android.util.Log;
     61 
     62 import libcore.io.IoUtils;
     63 
     64 import java.io.File;
     65 import java.io.FileInputStream;
     66 import java.io.FileNotFoundException;
     67 import java.io.IOException;
     68 import java.io.InputStream;
     69 import java.util.Collection;
     70 import java.util.HashMap;
     71 import java.util.Locale;
     72 import java.util.Map;
     73 
     74 /**
     75  * Presents a {@link DocumentsContract} view of {@link MediaProvider} external
     76  * contents.
     77  */
     78 public class MediaDocumentsProvider extends DocumentsProvider {
     79     private static final String TAG = "MediaDocumentsProvider";
     80 
     81     private static final String AUTHORITY = "com.android.providers.media.documents";
     82 
     83     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
     84             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
     85             Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_MIME_TYPES
     86     };
     87 
     88     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
     89             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
     90             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     91     };
     92 
     93     private static final String IMAGE_MIME_TYPES = joinNewline("image/*");
     94 
     95     private static final String VIDEO_MIME_TYPES = joinNewline("video/*");
     96 
     97     private static final String AUDIO_MIME_TYPES = joinNewline(
     98             "audio/*", "application/ogg", "application/x-flac");
     99 
    100     private static final String TYPE_IMAGES_ROOT = "images_root";
    101     private static final String TYPE_IMAGES_BUCKET = "images_bucket";
    102     private static final String TYPE_IMAGE = "image";
    103 
    104     private static final String TYPE_VIDEOS_ROOT = "videos_root";
    105     private static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
    106     private static final String TYPE_VIDEO = "video";
    107 
    108     private static final String TYPE_AUDIO_ROOT = "audio_root";
    109     private static final String TYPE_AUDIO = "audio";
    110     private static final String TYPE_ARTIST = "artist";
    111     private static final String TYPE_ALBUM = "album";
    112 
    113     private static boolean sReturnedImagesEmpty = false;
    114     private static boolean sReturnedVideosEmpty = false;
    115     private static boolean sReturnedAudioEmpty = false;
    116 
    117     private static String joinNewline(String... args) {
    118         return TextUtils.join("\n", args);
    119     }
    120 
    121     public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio";
    122     public static final String METADATA_KEY_VIDEO = "android.media.metadata.video";
    123     // Video lat/long are just that. Lat/long. Unlike EXIF where the values are
    124     // in fact some funky string encoding. So we add our own contstant to convey coords.
    125     public static final String METADATA_VIDEO_LATITUDE = "android.media.metadata.video:latitude";
    126     public static final String METADATA_VIDEO_LONGITUTE = "android.media.metadata.video:longitude";
    127 
    128     /*
    129      * A mapping between media colums and metadata tag names. These keys of the
    130      * map form the projection for queries against the media store database.
    131      */
    132     private static final Map<String, String> IMAGE_COLUMN_MAP = new HashMap<>();
    133     private static final Map<String, String> VIDEO_COLUMN_MAP = new HashMap<>();
    134     private static final Map<String, String> AUDIO_COLUMN_MAP = new HashMap<>();
    135 
    136     static {
    137         /**
    138          * Note that for images (jpegs at least) we'll first try an alternate
    139          * means of extracting metadata, one that provides more data. But if
    140          * that fails, or if the image type is not JPEG, we fall back to these columns.
    141          */
    142         IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH);
    143         IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH);
    144         IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME);
    145         IMAGE_COLUMN_MAP.put(ImageColumns.LATITUDE, ExifInterface.TAG_GPS_LATITUDE);
    146         IMAGE_COLUMN_MAP.put(ImageColumns.LONGITUDE, ExifInterface.TAG_GPS_LONGITUDE);
    147 
    148         VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION);
    149         VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH);
    150         VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH);
    151         VIDEO_COLUMN_MAP.put(VideoColumns.LATITUDE, METADATA_VIDEO_LATITUDE);
    152         VIDEO_COLUMN_MAP.put(VideoColumns.LONGITUDE, METADATA_VIDEO_LONGITUTE);
    153         VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE);
    154 
    155         AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST);
    156         AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER);
    157         AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM);
    158         AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR);
    159         AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION);
    160     }
    161 
    162     private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
    163         result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
    164     }
    165 
    166     @Override
    167     public boolean onCreate() {
    168         return true;
    169     }
    170 
    171     private void enforceShellRestrictions() {
    172         if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
    173                 && getContext().getSystemService(UserManager.class)
    174                         .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
    175             throw new SecurityException(
    176                     "Shell user cannot access files for user " + UserHandle.myUserId());
    177         }
    178     }
    179 
    180     @Override
    181     protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
    182             throws SecurityException {
    183         enforceShellRestrictions();
    184         return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
    185     }
    186 
    187     @Override
    188     protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
    189             throws SecurityException {
    190         enforceShellRestrictions();
    191         return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
    192     }
    193 
    194     private static void notifyRootsChanged(Context context) {
    195         context.getContentResolver()
    196                 .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
    197     }
    198 
    199     /**
    200      * When inserting the first item of each type, we need to trigger a roots
    201      * refresh to clear a previously reported {@link Root#FLAG_EMPTY}.
    202      */
    203     static void onMediaStoreInsert(Context context, String volumeName, int type, long id) {
    204         if (!"external".equals(volumeName)) return;
    205 
    206         if (type == FileColumns.MEDIA_TYPE_IMAGE && sReturnedImagesEmpty) {
    207             sReturnedImagesEmpty = false;
    208             notifyRootsChanged(context);
    209         } else if (type == FileColumns.MEDIA_TYPE_VIDEO && sReturnedVideosEmpty) {
    210             sReturnedVideosEmpty = false;
    211             notifyRootsChanged(context);
    212         } else if (type == FileColumns.MEDIA_TYPE_AUDIO && sReturnedAudioEmpty) {
    213             sReturnedAudioEmpty = false;
    214             notifyRootsChanged(context);
    215         }
    216     }
    217 
    218     /**
    219      * When deleting an item, we need to revoke any outstanding Uri grants.
    220      */
    221     static void onMediaStoreDelete(Context context, String volumeName, int type, long id) {
    222         if (!"external".equals(volumeName)) return;
    223 
    224         if (type == FileColumns.MEDIA_TYPE_IMAGE) {
    225             final Uri uri = DocumentsContract.buildDocumentUri(
    226                     AUTHORITY, getDocIdForIdent(TYPE_IMAGE, id));
    227             context.revokeUriPermission(uri, ~0);
    228         } else if (type == FileColumns.MEDIA_TYPE_VIDEO) {
    229             final Uri uri = DocumentsContract.buildDocumentUri(
    230                     AUTHORITY, getDocIdForIdent(TYPE_VIDEO, id));
    231             context.revokeUriPermission(uri, ~0);
    232         } else if (type == FileColumns.MEDIA_TYPE_AUDIO) {
    233             final Uri uri = DocumentsContract.buildDocumentUri(
    234                     AUTHORITY, getDocIdForIdent(TYPE_AUDIO, id));
    235             context.revokeUriPermission(uri, ~0);
    236         }
    237     }
    238 
    239     private static class Ident {
    240         public String type;
    241         public long id;
    242     }
    243 
    244     private static Ident getIdentForDocId(String docId) {
    245         final Ident ident = new Ident();
    246         final int split = docId.indexOf(':');
    247         if (split == -1) {
    248             ident.type = docId;
    249             ident.id = -1;
    250         } else {
    251             ident.type = docId.substring(0, split);
    252             ident.id = Long.parseLong(docId.substring(split + 1));
    253         }
    254         return ident;
    255     }
    256 
    257     private static String getDocIdForIdent(String type, long id) {
    258         return type + ":" + id;
    259     }
    260 
    261     private static String[] resolveRootProjection(String[] projection) {
    262         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
    263     }
    264 
    265     private static String[] resolveDocumentProjection(String[] projection) {
    266         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
    267     }
    268 
    269     private Uri getUriForDocumentId(String docId) {
    270         final Ident ident = getIdentForDocId(docId);
    271         if (TYPE_IMAGE.equals(ident.type) && ident.id != -1) {
    272             return ContentUris.withAppendedId(
    273                     Images.Media.EXTERNAL_CONTENT_URI, ident.id);
    274         } else if (TYPE_VIDEO.equals(ident.type) && ident.id != -1) {
    275             return ContentUris.withAppendedId(
    276                     Video.Media.EXTERNAL_CONTENT_URI, ident.id);
    277         } else if (TYPE_AUDIO.equals(ident.type) && ident.id != -1) {
    278             return ContentUris.withAppendedId(
    279                     Audio.Media.EXTERNAL_CONTENT_URI, ident.id);
    280         } else {
    281             throw new UnsupportedOperationException("Unsupported document " + docId);
    282         }
    283     }
    284 
    285     @Override
    286     public void deleteDocument(String docId) throws FileNotFoundException {
    287         final Uri target = getUriForDocumentId(docId);
    288 
    289         // Delegate to real provider
    290         final long token = Binder.clearCallingIdentity();
    291         try {
    292             getContext().getContentResolver().delete(target, null, null);
    293         } finally {
    294             Binder.restoreCallingIdentity(token);
    295         }
    296     }
    297 
    298     @Override
    299     public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException {
    300 
    301         String mimeType = getDocumentType(docId);
    302 
    303         if (MetadataReader.isSupportedMimeType(mimeType)) {
    304             return getDocumentMetadataFromStream(docId, mimeType);
    305         } else {
    306             return getDocumentMetadataFromIndex(docId);
    307         }
    308     }
    309 
    310     private @Nullable Bundle getDocumentMetadataFromStream(String docId, String mimeType) {
    311         assert MetadataReader.isSupportedMimeType(mimeType);
    312         InputStream stream = null;
    313         try {
    314             stream = new ParcelFileDescriptor.AutoCloseInputStream(
    315                     openDocument(docId, "r", null));
    316             Bundle metadata = new Bundle();
    317             MetadataReader.getMetadata(metadata, stream, mimeType, null);
    318             return metadata;
    319         } catch (IOException io) {
    320             return null;
    321         } finally {
    322             IoUtils.closeQuietly(stream);
    323         }
    324     }
    325 
    326     public @Nullable Bundle getDocumentMetadataFromIndex(String docId)
    327             throws FileNotFoundException {
    328 
    329         final Ident ident = getIdentForDocId(docId);
    330 
    331         Map<String, String> columnMap = null;
    332         String tagType;
    333         Uri query;
    334 
    335         switch (ident.type) {
    336             case TYPE_IMAGE:
    337                 columnMap = IMAGE_COLUMN_MAP;
    338                 tagType = DocumentsContract.METADATA_EXIF;
    339                 query = Images.Media.EXTERNAL_CONTENT_URI;
    340                 break;
    341             case TYPE_VIDEO:
    342                 columnMap = VIDEO_COLUMN_MAP;
    343                 tagType = METADATA_KEY_VIDEO;
    344                 query = Video.Media.EXTERNAL_CONTENT_URI;
    345                 break;
    346             case TYPE_AUDIO:
    347                 columnMap = AUDIO_COLUMN_MAP;
    348                 tagType = METADATA_KEY_AUDIO;
    349                 query = Audio.Media.EXTERNAL_CONTENT_URI;
    350                 break;
    351             default:
    352                 // Unsupported file type.
    353                 throw new FileNotFoundException(
    354                     "Metadata request for unsupported file type: " + ident.type);
    355         }
    356 
    357         final long token = Binder.clearCallingIdentity();
    358         Cursor cursor = null;
    359         Bundle result = null;
    360 
    361         final ContentResolver resolver = getContext().getContentResolver();
    362         Collection<String> columns = columnMap.keySet();
    363         String[] projection = columns.toArray(new String[columns.size()]);
    364         try {
    365             cursor = resolver.query(
    366                     query,
    367                     projection,
    368                     BaseColumns._ID + "=?",
    369                     new String[]{Long.toString(ident.id)},
    370                     null);
    371 
    372             if (!cursor.moveToFirst()) {
    373                 throw new FileNotFoundException("Can't find document id: " + docId);
    374             }
    375 
    376             final Bundle metadata = extractMetadataFromCursor(cursor, columnMap);
    377             result = new Bundle();
    378             result.putBundle(tagType, metadata);
    379             result.putStringArray(
    380                     DocumentsContract.METADATA_TYPES,
    381                     new String[]{tagType});
    382         } finally {
    383             IoUtils.closeQuietly(cursor);
    384             Binder.restoreCallingIdentity(token);
    385         }
    386         return result;
    387     }
    388 
    389     private static Bundle extractMetadataFromCursor(Cursor cursor, Map<String, String> columns) {
    390 
    391         assert (cursor.getCount() == 1);
    392 
    393         final Bundle metadata = new Bundle();
    394         for (String col : columns.keySet()) {
    395 
    396             int index = cursor.getColumnIndex(col);
    397             String bundleTag = columns.get(col);
    398 
    399             // Special case to be able to pull longs out of a cursor, as long is not a supported
    400             // field of getType.
    401             if (ExifInterface.TAG_DATETIME.equals(bundleTag)) {
    402                 // formate string to be consistent with how EXIF interface formats the date.
    403                 long date = cursor.getLong(index);
    404                 String format = DateFormat.getBestDateTimePattern(Locale.getDefault(),
    405                     "MMM dd, yyyy, hh:mm");
    406                 metadata.putString(bundleTag, DateFormat.format(format, date).toString());
    407                 continue;
    408             }
    409 
    410             switch (cursor.getType(index)) {
    411                 case Cursor.FIELD_TYPE_INTEGER:
    412                     metadata.putInt(bundleTag, cursor.getInt(index));
    413                     break;
    414                 case Cursor.FIELD_TYPE_FLOAT:
    415                     //Errors on the side of greater precision since interface doesnt support doubles
    416                     metadata.putFloat(bundleTag, cursor.getFloat(index));
    417                     break;
    418                 case Cursor.FIELD_TYPE_STRING:
    419                     metadata.putString(bundleTag, cursor.getString(index));
    420                     break;
    421                 case Cursor.FIELD_TYPE_BLOB:
    422                     Log.d(TAG, "Unsupported type, blob, for col: " + bundleTag);
    423                     break;
    424                 case Cursor.FIELD_TYPE_NULL:
    425                     Log.d(TAG, "Unsupported type, null, for col: " + bundleTag);
    426                     break;
    427                 default:
    428                     throw new RuntimeException("Data type not supported");
    429             }
    430         }
    431 
    432         return metadata;
    433     }
    434 
    435     @Override
    436     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
    437         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
    438         includeImagesRoot(result);
    439         includeVideosRoot(result);
    440         includeAudioRoot(result);
    441         return result;
    442     }
    443 
    444     @Override
    445     public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
    446         final ContentResolver resolver = getContext().getContentResolver();
    447         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    448         final Ident ident = getIdentForDocId(docId);
    449         final String[] queryArgs = new String[] { Long.toString(ident.id) } ;
    450 
    451         final long token = Binder.clearCallingIdentity();
    452         Cursor cursor = null;
    453         try {
    454             if (TYPE_IMAGES_ROOT.equals(ident.type)) {
    455                 // single root
    456                 includeImagesRootDocument(result);
    457             } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
    458                 // single bucket
    459                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
    460                         ImagesBucketQuery.PROJECTION, ImageColumns.BUCKET_ID + "=?",
    461                         queryArgs, ImagesBucketQuery.SORT_ORDER);
    462                 copyNotificationUri(result, cursor);
    463                 if (cursor.moveToFirst()) {
    464                     includeImagesBucket(result, cursor);
    465                 }
    466             } else if (TYPE_IMAGE.equals(ident.type)) {
    467                 // single image
    468                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
    469                         ImageQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
    470                         null);
    471                 copyNotificationUri(result, cursor);
    472                 if (cursor.moveToFirst()) {
    473                     includeImage(result, cursor);
    474                 }
    475             } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) {
    476                 // single root
    477                 includeVideosRootDocument(result);
    478             } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
    479                 // single bucket
    480                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
    481                         VideosBucketQuery.PROJECTION, VideoColumns.BUCKET_ID + "=?",
    482                         queryArgs, VideosBucketQuery.SORT_ORDER);
    483                 copyNotificationUri(result, cursor);
    484                 if (cursor.moveToFirst()) {
    485                     includeVideosBucket(result, cursor);
    486                 }
    487             } else if (TYPE_VIDEO.equals(ident.type)) {
    488                 // single video
    489                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
    490                         VideoQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
    491                         null);
    492                 copyNotificationUri(result, cursor);
    493                 if (cursor.moveToFirst()) {
    494                     includeVideo(result, cursor);
    495                 }
    496             } else if (TYPE_AUDIO_ROOT.equals(ident.type)) {
    497                 // single root
    498                 includeAudioRootDocument(result);
    499             } else if (TYPE_ARTIST.equals(ident.type)) {
    500                 // single artist
    501                 cursor = resolver.query(Artists.EXTERNAL_CONTENT_URI,
    502                         ArtistQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
    503                         null);
    504                 copyNotificationUri(result, cursor);
    505                 if (cursor.moveToFirst()) {
    506                     includeArtist(result, cursor);
    507                 }
    508             } else if (TYPE_ALBUM.equals(ident.type)) {
    509                 // single album
    510                 cursor = resolver.query(Albums.EXTERNAL_CONTENT_URI,
    511                         AlbumQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
    512                         null);
    513                 copyNotificationUri(result, cursor);
    514                 if (cursor.moveToFirst()) {
    515                     includeAlbum(result, cursor);
    516                 }
    517             } else if (TYPE_AUDIO.equals(ident.type)) {
    518                 // single song
    519                 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI,
    520                         SongQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
    521                         null);
    522                 copyNotificationUri(result, cursor);
    523                 if (cursor.moveToFirst()) {
    524                     includeAudio(result, cursor);
    525                 }
    526             } else {
    527                 throw new UnsupportedOperationException("Unsupported document " + docId);
    528             }
    529         } finally {
    530             IoUtils.closeQuietly(cursor);
    531             Binder.restoreCallingIdentity(token);
    532         }
    533         return result;
    534     }
    535 
    536     @Override
    537     public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
    538             throws FileNotFoundException {
    539         final ContentResolver resolver = getContext().getContentResolver();
    540         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    541         final Ident ident = getIdentForDocId(docId);
    542         final String[] queryArgs = new String[] { Long.toString(ident.id) } ;
    543 
    544         final long token = Binder.clearCallingIdentity();
    545         Cursor cursor = null;
    546         try {
    547             if (TYPE_IMAGES_ROOT.equals(ident.type)) {
    548                 // include all unique buckets
    549                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
    550                         ImagesBucketQuery.PROJECTION, null, null, ImagesBucketQuery.SORT_ORDER);
    551                 // multiple orders
    552                 copyNotificationUri(result, cursor);
    553                 long lastId = Long.MIN_VALUE;
    554                 while (cursor.moveToNext()) {
    555                     final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID);
    556                     if (lastId != id) {
    557                         includeImagesBucket(result, cursor);
    558                         lastId = id;
    559                     }
    560                 }
    561             } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
    562                 // include images under bucket
    563                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
    564                         ImageQuery.PROJECTION, ImageColumns.BUCKET_ID + "=?",
    565                         queryArgs, null);
    566                 copyNotificationUri(result, cursor);
    567                 while (cursor.moveToNext()) {
    568                     includeImage(result, cursor);
    569                 }
    570             } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) {
    571                 // include all unique buckets
    572                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
    573                         VideosBucketQuery.PROJECTION, null, null, VideosBucketQuery.SORT_ORDER);
    574                 copyNotificationUri(result, cursor);
    575                 long lastId = Long.MIN_VALUE;
    576                 while (cursor.moveToNext()) {
    577                     final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID);
    578                     if (lastId != id) {
    579                         includeVideosBucket(result, cursor);
    580                         lastId = id;
    581                     }
    582                 }
    583             } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
    584                 // include videos under bucket
    585                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
    586                         VideoQuery.PROJECTION, VideoColumns.BUCKET_ID + "=?",
    587                         queryArgs, null);
    588                 copyNotificationUri(result, cursor);
    589                 while (cursor.moveToNext()) {
    590                     includeVideo(result, cursor);
    591                 }
    592             } else if (TYPE_AUDIO_ROOT.equals(ident.type)) {
    593                 // include all artists
    594                 cursor = resolver.query(Audio.Artists.EXTERNAL_CONTENT_URI,
    595                         ArtistQuery.PROJECTION, null, null, null);
    596                 copyNotificationUri(result, cursor);
    597                 while (cursor.moveToNext()) {
    598                     includeArtist(result, cursor);
    599                 }
    600             } else if (TYPE_ARTIST.equals(ident.type)) {
    601                 // include all albums under artist
    602                 cursor = resolver.query(Artists.Albums.getContentUri("external", ident.id),
    603                         AlbumQuery.PROJECTION, null, null, null);
    604                 copyNotificationUri(result, cursor);
    605                 while (cursor.moveToNext()) {
    606                     includeAlbum(result, cursor);
    607                 }
    608             } else if (TYPE_ALBUM.equals(ident.type)) {
    609                 // include all songs under album
    610                 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI,
    611                         SongQuery.PROJECTION, AudioColumns.ALBUM_ID + "=?",
    612                         queryArgs, null);
    613                 copyNotificationUri(result, cursor);
    614                 while (cursor.moveToNext()) {
    615                     includeAudio(result, cursor);
    616                 }
    617             } else {
    618                 throw new UnsupportedOperationException("Unsupported document " + docId);
    619             }
    620         } finally {
    621             IoUtils.closeQuietly(cursor);
    622             Binder.restoreCallingIdentity(token);
    623         }
    624         return result;
    625     }
    626 
    627     @Override
    628     public Cursor queryRecentDocuments(String rootId, String[] projection)
    629             throws FileNotFoundException {
    630         final ContentResolver resolver = getContext().getContentResolver();
    631         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    632 
    633         final long token = Binder.clearCallingIdentity();
    634         Cursor cursor = null;
    635         try {
    636             if (TYPE_IMAGES_ROOT.equals(rootId)) {
    637                 // include all unique buckets
    638                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
    639                         ImageQuery.PROJECTION, null, null, ImageColumns.DATE_MODIFIED + " DESC");
    640                 copyNotificationUri(result, cursor);
    641                 while (cursor.moveToNext() && result.getCount() < 64) {
    642                     includeImage(result, cursor);
    643                 }
    644             } else if (TYPE_VIDEOS_ROOT.equals(rootId)) {
    645                 // include all unique buckets
    646                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
    647                         VideoQuery.PROJECTION, null, null, VideoColumns.DATE_MODIFIED + " DESC");
    648                 copyNotificationUri(result, cursor);
    649                 while (cursor.moveToNext() && result.getCount() < 64) {
    650                     includeVideo(result, cursor);
    651                 }
    652             } else {
    653                 throw new UnsupportedOperationException("Unsupported root " + rootId);
    654             }
    655         } finally {
    656             IoUtils.closeQuietly(cursor);
    657             Binder.restoreCallingIdentity(token);
    658         }
    659         return result;
    660     }
    661 
    662     @Override
    663     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
    664             throws FileNotFoundException {
    665         final ContentResolver resolver = getContext().getContentResolver();
    666         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    667 
    668         final long token = Binder.clearCallingIdentity();
    669         final String[] queryArgs = new String[] { "%" + query + "%" };
    670         Cursor cursor = null;
    671         try {
    672             if (TYPE_IMAGES_ROOT.equals(rootId)) {
    673                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, ImageQuery.PROJECTION,
    674                         ImageColumns.DISPLAY_NAME + " LIKE ?", queryArgs,
    675                         ImageColumns.DATE_MODIFIED + " DESC");
    676                 copyNotificationUri(result, cursor);
    677                 while (cursor.moveToNext()) {
    678                     includeImage(result, cursor);
    679                 }
    680             } else if (TYPE_VIDEOS_ROOT.equals(rootId)) {
    681                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, VideoQuery.PROJECTION,
    682                         VideoColumns.DISPLAY_NAME + " LIKE ?", queryArgs,
    683                         VideoColumns.DATE_MODIFIED + " DESC");
    684                 copyNotificationUri(result, cursor);
    685                 while (cursor.moveToNext()) {
    686                     includeVideo(result, cursor);
    687                 }
    688             } else if (TYPE_AUDIO_ROOT.equals(rootId)) {
    689                 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, SongQuery.PROJECTION,
    690                         AudioColumns.TITLE + " LIKE ?", queryArgs,
    691                         AudioColumns.DATE_MODIFIED + " DESC");
    692                 copyNotificationUri(result, cursor);
    693                 while (cursor.moveToNext()) {
    694                     includeAudio(result, cursor);
    695                 }
    696             } else {
    697                 throw new UnsupportedOperationException("Unsupported root " + rootId);
    698             }
    699         } finally {
    700             IoUtils.closeQuietly(cursor);
    701             Binder.restoreCallingIdentity(token);
    702         }
    703         return result;
    704     }
    705 
    706     @Override
    707     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
    708             throws FileNotFoundException {
    709         final Uri target = getUriForDocumentId(docId);
    710 
    711         if (!"r".equals(mode)) {
    712             throw new IllegalArgumentException("Media is read-only");
    713         }
    714 
    715         // Delegate to real provider
    716         final long token = Binder.clearCallingIdentity();
    717         try {
    718             return getContext().getContentResolver().openFileDescriptor(target, mode);
    719         } finally {
    720             Binder.restoreCallingIdentity(token);
    721         }
    722     }
    723 
    724     @Override
    725     public AssetFileDescriptor openDocumentThumbnail(
    726             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
    727         final Ident ident = getIdentForDocId(docId);
    728 
    729         final long token = Binder.clearCallingIdentity();
    730         try {
    731             if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
    732                 final long id = getImageForBucketCleared(ident.id);
    733                 return openOrCreateImageThumbnailCleared(id, signal);
    734             } else if (TYPE_IMAGE.equals(ident.type)) {
    735                 return openOrCreateImageThumbnailCleared(ident.id, signal);
    736             } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
    737                 final long id = getVideoForBucketCleared(ident.id);
    738                 return openOrCreateVideoThumbnailCleared(id, signal);
    739             } else if (TYPE_VIDEO.equals(ident.type)) {
    740                 return openOrCreateVideoThumbnailCleared(ident.id, signal);
    741             } else {
    742                 throw new UnsupportedOperationException("Unsupported document " + docId);
    743             }
    744         } finally {
    745             Binder.restoreCallingIdentity(token);
    746         }
    747     }
    748 
    749     private boolean isEmpty(Uri uri) {
    750         final ContentResolver resolver = getContext().getContentResolver();
    751         final long token = Binder.clearCallingIdentity();
    752         Cursor cursor = null;
    753         try {
    754             cursor = resolver.query(uri, new String[] {
    755                     BaseColumns._ID }, null, null, null);
    756             return (cursor == null) || (cursor.getCount() == 0);
    757         } finally {
    758             IoUtils.closeQuietly(cursor);
    759             Binder.restoreCallingIdentity(token);
    760         }
    761     }
    762 
    763     private void includeImagesRoot(MatrixCursor result) {
    764         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH;
    765         if (isEmpty(Images.Media.EXTERNAL_CONTENT_URI)) {
    766             flags |= Root.FLAG_EMPTY;
    767             sReturnedImagesEmpty = true;
    768         }
    769 
    770         final RowBuilder row = result.newRow();
    771         row.add(Root.COLUMN_ROOT_ID, TYPE_IMAGES_ROOT);
    772         row.add(Root.COLUMN_FLAGS, flags);
    773         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_images));
    774         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT);
    775         row.add(Root.COLUMN_MIME_TYPES, IMAGE_MIME_TYPES);
    776     }
    777 
    778     private void includeVideosRoot(MatrixCursor result) {
    779         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH;
    780         if (isEmpty(Video.Media.EXTERNAL_CONTENT_URI)) {
    781             flags |= Root.FLAG_EMPTY;
    782             sReturnedVideosEmpty = true;
    783         }
    784 
    785         final RowBuilder row = result.newRow();
    786         row.add(Root.COLUMN_ROOT_ID, TYPE_VIDEOS_ROOT);
    787         row.add(Root.COLUMN_FLAGS, flags);
    788         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_videos));
    789         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT);
    790         row.add(Root.COLUMN_MIME_TYPES, VIDEO_MIME_TYPES);
    791     }
    792 
    793     private void includeAudioRoot(MatrixCursor result) {
    794         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH;
    795         if (isEmpty(Audio.Media.EXTERNAL_CONTENT_URI)) {
    796             flags |= Root.FLAG_EMPTY;
    797             sReturnedAudioEmpty = true;
    798         }
    799 
    800         final RowBuilder row = result.newRow();
    801         row.add(Root.COLUMN_ROOT_ID, TYPE_AUDIO_ROOT);
    802         row.add(Root.COLUMN_FLAGS, flags);
    803         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_audio));
    804         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT);
    805         row.add(Root.COLUMN_MIME_TYPES, AUDIO_MIME_TYPES);
    806     }
    807 
    808     private void includeImagesRootDocument(MatrixCursor result) {
    809         final RowBuilder row = result.newRow();
    810         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT);
    811         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_images));
    812         row.add(Document.COLUMN_FLAGS,
    813                 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
    814         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    815     }
    816 
    817     private void includeVideosRootDocument(MatrixCursor result) {
    818         final RowBuilder row = result.newRow();
    819         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT);
    820         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_videos));
    821         row.add(Document.COLUMN_FLAGS,
    822                 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
    823         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    824     }
    825 
    826     private void includeAudioRootDocument(MatrixCursor result) {
    827         final RowBuilder row = result.newRow();
    828         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT);
    829         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_audio));
    830         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    831     }
    832 
    833     private interface ImagesBucketQuery {
    834         final String[] PROJECTION = new String[] {
    835                 ImageColumns.BUCKET_ID,
    836                 ImageColumns.BUCKET_DISPLAY_NAME,
    837                 ImageColumns.DATE_MODIFIED };
    838         final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED
    839                 + " DESC";
    840 
    841         final int BUCKET_ID = 0;
    842         final int BUCKET_DISPLAY_NAME = 1;
    843         final int DATE_MODIFIED = 2;
    844     }
    845 
    846     private void includeImagesBucket(MatrixCursor result, Cursor cursor) {
    847         final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID);
    848         final String docId = getDocIdForIdent(TYPE_IMAGES_BUCKET, id);
    849 
    850         final RowBuilder row = result.newRow();
    851         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    852         row.add(Document.COLUMN_DISPLAY_NAME,
    853                 cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME));
    854         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    855         row.add(Document.COLUMN_LAST_MODIFIED,
    856                 cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
    857         row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID
    858                 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
    859     }
    860 
    861     private interface ImageQuery {
    862         final String[] PROJECTION = new String[] {
    863                 ImageColumns._ID,
    864                 ImageColumns.DISPLAY_NAME,
    865                 ImageColumns.MIME_TYPE,
    866                 ImageColumns.SIZE,
    867                 ImageColumns.DATE_MODIFIED };
    868 
    869         final int _ID = 0;
    870         final int DISPLAY_NAME = 1;
    871         final int MIME_TYPE = 2;
    872         final int SIZE = 3;
    873         final int DATE_MODIFIED = 4;
    874     }
    875 
    876     private void includeImage(MatrixCursor result, Cursor cursor) {
    877         final long id = cursor.getLong(ImageQuery._ID);
    878         final String docId = getDocIdForIdent(TYPE_IMAGE, id);
    879 
    880         final RowBuilder row = result.newRow();
    881         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    882         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ImageQuery.DISPLAY_NAME));
    883         row.add(Document.COLUMN_SIZE, cursor.getLong(ImageQuery.SIZE));
    884         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(ImageQuery.MIME_TYPE));
    885         row.add(Document.COLUMN_LAST_MODIFIED,
    886                 cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
    887         row.add(Document.COLUMN_FLAGS,
    888                 Document.FLAG_SUPPORTS_THUMBNAIL
    889                     | Document.FLAG_SUPPORTS_DELETE
    890                     | Document.FLAG_SUPPORTS_METADATA);
    891     }
    892 
    893     private interface VideosBucketQuery {
    894         final String[] PROJECTION = new String[] {
    895                 VideoColumns.BUCKET_ID,
    896                 VideoColumns.BUCKET_DISPLAY_NAME,
    897                 VideoColumns.DATE_MODIFIED };
    898         final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED
    899                 + " DESC";
    900 
    901         final int BUCKET_ID = 0;
    902         final int BUCKET_DISPLAY_NAME = 1;
    903         final int DATE_MODIFIED = 2;
    904     }
    905 
    906     private void includeVideosBucket(MatrixCursor result, Cursor cursor) {
    907         final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID);
    908         final String docId = getDocIdForIdent(TYPE_VIDEOS_BUCKET, id);
    909 
    910         final RowBuilder row = result.newRow();
    911         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    912         row.add(Document.COLUMN_DISPLAY_NAME,
    913                 cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME));
    914         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    915         row.add(Document.COLUMN_LAST_MODIFIED,
    916                 cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
    917         row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID
    918                 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
    919     }
    920 
    921     private interface VideoQuery {
    922         final String[] PROJECTION = new String[] {
    923                 VideoColumns._ID,
    924                 VideoColumns.DISPLAY_NAME,
    925                 VideoColumns.MIME_TYPE,
    926                 VideoColumns.SIZE,
    927                 VideoColumns.DATE_MODIFIED };
    928 
    929         final int _ID = 0;
    930         final int DISPLAY_NAME = 1;
    931         final int MIME_TYPE = 2;
    932         final int SIZE = 3;
    933         final int DATE_MODIFIED = 4;
    934     }
    935 
    936     private void includeVideo(MatrixCursor result, Cursor cursor) {
    937         final long id = cursor.getLong(VideoQuery._ID);
    938         final String docId = getDocIdForIdent(TYPE_VIDEO, id);
    939 
    940         final RowBuilder row = result.newRow();
    941         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    942         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(VideoQuery.DISPLAY_NAME));
    943         row.add(Document.COLUMN_SIZE, cursor.getLong(VideoQuery.SIZE));
    944         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(VideoQuery.MIME_TYPE));
    945         row.add(Document.COLUMN_LAST_MODIFIED,
    946                 cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
    947         row.add(Document.COLUMN_FLAGS,
    948                 Document.FLAG_SUPPORTS_THUMBNAIL
    949                     | Document.FLAG_SUPPORTS_DELETE
    950                     | Document.FLAG_SUPPORTS_METADATA);
    951     }
    952 
    953     private interface ArtistQuery {
    954         final String[] PROJECTION = new String[] {
    955                 BaseColumns._ID,
    956                 ArtistColumns.ARTIST };
    957 
    958         final int _ID = 0;
    959         final int ARTIST = 1;
    960     }
    961 
    962     private void includeArtist(MatrixCursor result, Cursor cursor) {
    963         final long id = cursor.getLong(ArtistQuery._ID);
    964         final String docId = getDocIdForIdent(TYPE_ARTIST, id);
    965 
    966         final RowBuilder row = result.newRow();
    967         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    968         row.add(Document.COLUMN_DISPLAY_NAME,
    969                 cleanUpMediaDisplayName(cursor.getString(ArtistQuery.ARTIST)));
    970         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    971     }
    972 
    973     private interface AlbumQuery {
    974         final String[] PROJECTION = new String[] {
    975                 BaseColumns._ID,
    976                 AlbumColumns.ALBUM };
    977 
    978         final int _ID = 0;
    979         final int ALBUM = 1;
    980     }
    981 
    982     private void includeAlbum(MatrixCursor result, Cursor cursor) {
    983         final long id = cursor.getLong(AlbumQuery._ID);
    984         final String docId = getDocIdForIdent(TYPE_ALBUM, id);
    985 
    986         final RowBuilder row = result.newRow();
    987         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    988         row.add(Document.COLUMN_DISPLAY_NAME,
    989                 cleanUpMediaDisplayName(cursor.getString(AlbumQuery.ALBUM)));
    990         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    991     }
    992 
    993     private interface SongQuery {
    994         final String[] PROJECTION = new String[] {
    995                 AudioColumns._ID,
    996                 AudioColumns.TITLE,
    997                 AudioColumns.MIME_TYPE,
    998                 AudioColumns.SIZE,
    999                 AudioColumns.DATE_MODIFIED };
   1000 
   1001         final int _ID = 0;
   1002         final int TITLE = 1;
   1003         final int MIME_TYPE = 2;
   1004         final int SIZE = 3;
   1005         final int DATE_MODIFIED = 4;
   1006     }
   1007 
   1008     private void includeAudio(MatrixCursor result, Cursor cursor) {
   1009         final long id = cursor.getLong(SongQuery._ID);
   1010         final String docId = getDocIdForIdent(TYPE_AUDIO, id);
   1011 
   1012         final RowBuilder row = result.newRow();
   1013         row.add(Document.COLUMN_DOCUMENT_ID, docId);
   1014         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(SongQuery.TITLE));
   1015         row.add(Document.COLUMN_SIZE, cursor.getLong(SongQuery.SIZE));
   1016         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE));
   1017         row.add(Document.COLUMN_LAST_MODIFIED,
   1018                 cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
   1019         row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE
   1020                 | Document.FLAG_SUPPORTS_METADATA);
   1021     }
   1022 
   1023     private interface ImagesBucketThumbnailQuery {
   1024         final String[] PROJECTION = new String[] {
   1025                 ImageColumns._ID,
   1026                 ImageColumns.BUCKET_ID,
   1027                 ImageColumns.DATE_MODIFIED };
   1028 
   1029         final int _ID = 0;
   1030         final int BUCKET_ID = 1;
   1031         final int DATE_MODIFIED = 2;
   1032     }
   1033 
   1034     private long getImageForBucketCleared(long bucketId) throws FileNotFoundException {
   1035         final ContentResolver resolver = getContext().getContentResolver();
   1036         Cursor cursor = null;
   1037         try {
   1038             cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
   1039                     ImagesBucketThumbnailQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + bucketId,
   1040                     null, ImageColumns.DATE_MODIFIED + " DESC");
   1041             if (cursor.moveToFirst()) {
   1042                 return cursor.getLong(ImagesBucketThumbnailQuery._ID);
   1043             }
   1044         } finally {
   1045             IoUtils.closeQuietly(cursor);
   1046         }
   1047         throw new FileNotFoundException("No video found for bucket");
   1048     }
   1049 
   1050     private interface ImageThumbnailQuery {
   1051         final String[] PROJECTION = new String[] {
   1052                 Images.Thumbnails.DATA };
   1053 
   1054         final int _DATA = 0;
   1055     }
   1056 
   1057     private ParcelFileDescriptor openImageThumbnailCleared(long id, CancellationSignal signal)
   1058             throws FileNotFoundException {
   1059         final ContentResolver resolver = getContext().getContentResolver();
   1060 
   1061         Cursor cursor = null;
   1062         try {
   1063             cursor = resolver.query(Images.Thumbnails.EXTERNAL_CONTENT_URI,
   1064                     ImageThumbnailQuery.PROJECTION, Images.Thumbnails.IMAGE_ID + "=" + id, null,
   1065                     null, signal);
   1066             if (cursor.moveToFirst()) {
   1067                 final String data = cursor.getString(ImageThumbnailQuery._DATA);
   1068                 return ParcelFileDescriptor.open(
   1069                         new File(data), ParcelFileDescriptor.MODE_READ_ONLY);
   1070             }
   1071         } finally {
   1072             IoUtils.closeQuietly(cursor);
   1073         }
   1074         return null;
   1075     }
   1076 
   1077     private AssetFileDescriptor openOrCreateImageThumbnailCleared(
   1078             long id, CancellationSignal signal) throws FileNotFoundException {
   1079         final ContentResolver resolver = getContext().getContentResolver();
   1080 
   1081         ParcelFileDescriptor pfd = openImageThumbnailCleared(id, signal);
   1082         if (pfd == null) {
   1083             // No thumbnail yet, so generate. This is messy, since we drop the
   1084             // Bitmap on the floor, but its the least-complicated way.
   1085             final BitmapFactory.Options opts = new BitmapFactory.Options();
   1086             opts.inJustDecodeBounds = true;
   1087             Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, opts);
   1088 
   1089             pfd = openImageThumbnailCleared(id, signal);
   1090         }
   1091 
   1092         if (pfd == null) {
   1093             // Phoey, fallback to full image
   1094             final Uri fullUri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id);
   1095             pfd = resolver.openFileDescriptor(fullUri, "r", signal);
   1096         }
   1097 
   1098         final int orientation = queryOrientationForImage(id, signal);
   1099         final Bundle extras;
   1100         if (orientation != 0) {
   1101             extras = new Bundle(1);
   1102             extras.putInt(DocumentsContract.EXTRA_ORIENTATION, orientation);
   1103         } else {
   1104             extras = null;
   1105         }
   1106 
   1107         return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, extras);
   1108     }
   1109 
   1110     private interface VideosBucketThumbnailQuery {
   1111         final String[] PROJECTION = new String[] {
   1112                 VideoColumns._ID,
   1113                 VideoColumns.BUCKET_ID,
   1114                 VideoColumns.DATE_MODIFIED };
   1115 
   1116         final int _ID = 0;
   1117         final int BUCKET_ID = 1;
   1118         final int DATE_MODIFIED = 2;
   1119     }
   1120 
   1121     private long getVideoForBucketCleared(long bucketId)
   1122             throws FileNotFoundException {
   1123         final ContentResolver resolver = getContext().getContentResolver();
   1124         Cursor cursor = null;
   1125         try {
   1126             cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
   1127                     VideosBucketThumbnailQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + bucketId,
   1128                     null, VideoColumns.DATE_MODIFIED + " DESC");
   1129             if (cursor.moveToFirst()) {
   1130                 return cursor.getLong(VideosBucketThumbnailQuery._ID);
   1131             }
   1132         } finally {
   1133             IoUtils.closeQuietly(cursor);
   1134         }
   1135         throw new FileNotFoundException("No video found for bucket");
   1136     }
   1137 
   1138     private interface VideoThumbnailQuery {
   1139         final String[] PROJECTION = new String[] {
   1140                 Video.Thumbnails.DATA };
   1141 
   1142         final int _DATA = 0;
   1143     }
   1144 
   1145     private AssetFileDescriptor openVideoThumbnailCleared(long id, CancellationSignal signal)
   1146             throws FileNotFoundException {
   1147         final ContentResolver resolver = getContext().getContentResolver();
   1148         Cursor cursor = null;
   1149         try {
   1150             cursor = resolver.query(Video.Thumbnails.EXTERNAL_CONTENT_URI,
   1151                     VideoThumbnailQuery.PROJECTION, Video.Thumbnails.VIDEO_ID + "=" + id, null,
   1152                     null, signal);
   1153             if (cursor.moveToFirst()) {
   1154                 final String data = cursor.getString(VideoThumbnailQuery._DATA);
   1155                 return new AssetFileDescriptor(ParcelFileDescriptor.open(
   1156                         new File(data), ParcelFileDescriptor.MODE_READ_ONLY), 0,
   1157                         AssetFileDescriptor.UNKNOWN_LENGTH);
   1158             }
   1159         } finally {
   1160             IoUtils.closeQuietly(cursor);
   1161         }
   1162         return null;
   1163     }
   1164 
   1165     private AssetFileDescriptor openOrCreateVideoThumbnailCleared(
   1166             long id, CancellationSignal signal) throws FileNotFoundException {
   1167         final ContentResolver resolver = getContext().getContentResolver();
   1168 
   1169         AssetFileDescriptor afd = openVideoThumbnailCleared(id, signal);
   1170         if (afd == null) {
   1171             // No thumbnail yet, so generate. This is messy, since we drop the
   1172             // Bitmap on the floor, but its the least-complicated way.
   1173             final BitmapFactory.Options opts = new BitmapFactory.Options();
   1174             opts.inJustDecodeBounds = true;
   1175             Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, opts);
   1176 
   1177             afd = openVideoThumbnailCleared(id, signal);
   1178         }
   1179 
   1180         return afd;
   1181     }
   1182 
   1183     private interface ImageOrientationQuery {
   1184         final String[] PROJECTION = new String[] {
   1185                 ImageColumns.ORIENTATION };
   1186 
   1187         final int ORIENTATION = 0;
   1188     }
   1189 
   1190     private int queryOrientationForImage(long id, CancellationSignal signal) {
   1191         final ContentResolver resolver = getContext().getContentResolver();
   1192 
   1193         Cursor cursor = null;
   1194         try {
   1195             cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
   1196                     ImageOrientationQuery.PROJECTION, ImageColumns._ID + "=" + id, null, null,
   1197                     signal);
   1198             if (cursor.moveToFirst()) {
   1199                 return cursor.getInt(ImageOrientationQuery.ORIENTATION);
   1200             } else {
   1201                 Log.w(TAG, "Missing orientation data for " + id);
   1202                 return 0;
   1203             }
   1204         } finally {
   1205             IoUtils.closeQuietly(cursor);
   1206         }
   1207     }
   1208 
   1209     private String cleanUpMediaDisplayName(String displayName) {
   1210         if (!MediaStore.UNKNOWN_STRING.equals(displayName)) {
   1211             return displayName;
   1212         }
   1213         return getContext().getResources().getString(com.android.internal.R.string.unknownName);
   1214     }
   1215 }
   1216