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