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