1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.res.AssetFileDescriptor;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.database.MatrixCursor.RowBuilder;
26 import android.graphics.BitmapFactory;
27 import android.graphics.Point;
28 import android.net.Uri;
29 import android.os.Binder;
30 import android.os.Bundle;
31 import android.os.CancellationSignal;
32 import android.os.ParcelFileDescriptor;
33 import android.provider.BaseColumns;
34 import android.provider.DocumentsContract;
35 import android.provider.DocumentsContract.Document;
36 import android.provider.DocumentsContract.Root;
37 import android.provider.DocumentsProvider;
38 import android.provider.MediaStore.Audio;
39 import android.provider.MediaStore.Audio.AlbumColumns;
40 import android.provider.MediaStore.Audio.Albums;
41 import android.provider.MediaStore.Audio.ArtistColumns;
42 import android.provider.MediaStore.Audio.Artists;
43 import android.provider.MediaStore.Audio.AudioColumns;
44 import android.provider.MediaStore.Files.FileColumns;
45 import android.provider.MediaStore.Images;
46 import android.provider.MediaStore.Images.ImageColumns;
47 import android.provider.MediaStore.Video;
48 import android.provider.MediaStore.Video.VideoColumns;
49 import android.text.TextUtils;
50 import android.text.format.DateUtils;
51 import android.util.Log;
52 
53 import libcore.io.IoUtils;
54 
55 import java.io.File;
56 import java.io.FileNotFoundException;
57 
58 /**
59  * Presents a {@link DocumentsContract} view of {@link MediaProvider} external
60  * contents.
61  */
62 public 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 
joinNewline(String... args)101     private static String joinNewline(String... args) {
102         return TextUtils.join("\n", args);
103     }
104 
copyNotificationUri(MatrixCursor result, Cursor cursor)105     private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
106         result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
107     }
108 
109     @Override
onCreate()110     public boolean onCreate() {
111         return true;
112     }
113 
notifyRootsChanged(Context context)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      */
onMediaStoreInsert(Context context, String volumeName, int type, long id)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      */
onMediaStoreDelete(Context context, String volumeName, int type, long id)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 
getIdentForDocId(String docId)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 
getDocIdForIdent(String type, long id)177     private static String getDocIdForIdent(String type, long id) {
178         return type + ":" + id;
179     }
180 
resolveRootProjection(String[] projection)181     private static String[] resolveRootProjection(String[] projection) {
182         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
183     }
184 
resolveDocumentProjection(String[] projection)185     private static String[] resolveDocumentProjection(String[] projection) {
186         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
187     }
188 
189     @Override
queryRoots(String[] projection)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
queryDocument(String docId, String[] projection)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
queryChildDocuments(String docId, String[] projection, String sortOrder)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
queryRecentDocuments(String rootId, String[] projection)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
openDocument(String docId, String mode, CancellationSignal signal)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
openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)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 
isEmpty(Uri uri)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 == null) || (cursor.getCount() == 0);
480         } finally {
481             IoUtils.closeQuietly(cursor);
482             Binder.restoreCallingIdentity(token);
483         }
484     }
485 
includeImagesRoot(MatrixCursor result)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 
includeVideosRoot(MatrixCursor result)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 
includeAudioRoot(MatrixCursor result)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 
includeImagesRootDocument(MatrixCursor result)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 
includeVideosRootDocument(MatrixCursor result)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 
includeAudioRootDocument(MatrixCursor result)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 
includeImagesBucket(MatrixCursor result, Cursor cursor)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 
includeImage(MatrixCursor result, Cursor cursor)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 
includeVideosBucket(MatrixCursor result, Cursor cursor)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 
includeVideo(MatrixCursor result, Cursor cursor)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 
includeArtist(MatrixCursor result, Cursor cursor)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 
includeAlbum(MatrixCursor result, Cursor cursor)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 
includeAudio(MatrixCursor result, Cursor cursor)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 
getImageForBucketCleared(long bucketId)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 
openImageThumbnailCleared(long id, CancellationSignal signal)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 
openOrCreateImageThumbnailCleared( long id, CancellationSignal signal)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 
getVideoForBucketCleared(long bucketId)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 
openVideoThumbnailCleared(long id, CancellationSignal signal)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 
openOrCreateVideoThumbnailCleared( long id, CancellationSignal signal)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 
queryOrientationForImage(long id, CancellationSignal signal)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