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