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