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