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