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 static android.content.ContentResolver.EXTRA_SIZE;
20 import static android.provider.DocumentsContract.QUERY_ARG_DISPLAY_NAME;
21 import static android.provider.DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA;
22 import static android.provider.DocumentsContract.QUERY_ARG_FILE_SIZE_OVER;
23 import static android.provider.DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER;
24 import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES;
25 
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.Context;
29 import android.content.res.AssetFileDescriptor;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.database.MatrixCursor.RowBuilder;
33 import android.graphics.Point;
34 import android.media.ExifInterface;
35 import android.media.MediaMetadata;
36 import android.net.Uri;
37 import android.os.Binder;
38 import android.os.Bundle;
39 import android.os.CancellationSignal;
40 import android.os.ParcelFileDescriptor;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.provider.BaseColumns;
44 import android.provider.DocumentsContract;
45 import android.provider.DocumentsContract.Document;
46 import android.provider.DocumentsContract.Root;
47 import android.provider.DocumentsProvider;
48 import android.provider.MediaStore;
49 import android.provider.MediaStore.Audio;
50 import android.provider.MediaStore.Audio.AlbumColumns;
51 import android.provider.MediaStore.Audio.Albums;
52 import android.provider.MediaStore.Audio.ArtistColumns;
53 import android.provider.MediaStore.Audio.Artists;
54 import android.provider.MediaStore.Audio.AudioColumns;
55 import android.provider.MediaStore.Files;
56 import android.provider.MediaStore.Files.FileColumns;
57 import android.provider.MediaStore.Images;
58 import android.provider.MediaStore.Images.ImageColumns;
59 import android.provider.MediaStore.Video;
60 import android.provider.MediaStore.Video.VideoColumns;
61 import android.text.TextUtils;
62 import android.text.format.DateFormat;
63 import android.text.format.DateUtils;
64 import android.util.Log;
65 import android.util.Pair;
66 
67 import androidx.annotation.Nullable;
68 import androidx.core.content.MimeTypeFilter;
69 
70 import com.android.providers.media.util.FileUtils;
71 
72 import java.io.FileNotFoundException;
73 import java.util.ArrayList;
74 import java.util.Collection;
75 import java.util.HashMap;
76 import java.util.List;
77 import java.util.Locale;
78 import java.util.Map;
79 import java.util.Objects;
80 
81 /**
82  * Presents a {@link DocumentsContract} view of {@link MediaProvider} external
83  * contents.
84  */
85 public class MediaDocumentsProvider extends DocumentsProvider {
86     private static final String TAG = "MediaDocumentsProvider";
87 
88     public static final String AUTHORITY = "com.android.providers.media.documents";
89 
90     private static final String SUPPORTED_QUERY_ARGS = joinNewline(
91             DocumentsContract.QUERY_ARG_DISPLAY_NAME,
92             DocumentsContract.QUERY_ARG_FILE_SIZE_OVER,
93             DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER,
94             DocumentsContract.QUERY_ARG_MIME_TYPES);
95 
96     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
97             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
98             Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_MIME_TYPES,
99             Root.COLUMN_QUERY_ARGS
100     };
101 
102     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
103             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
104             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
105     };
106 
107     private static final String IMAGE_MIME_TYPES = joinNewline("image/*");
108 
109     private static final String VIDEO_MIME_TYPES = joinNewline("video/*");
110 
111     private static final String AUDIO_MIME_TYPES = joinNewline(
112             "audio/*", "application/ogg", "application/x-flac");
113 
114     private static final String DOCUMENT_MIME_TYPES = joinNewline("*/*");
115 
116     static final String TYPE_IMAGES_ROOT = "images_root";
117     static final String TYPE_IMAGES_BUCKET = "images_bucket";
118     static final String TYPE_IMAGE = "image";
119 
120     static final String TYPE_VIDEOS_ROOT = "videos_root";
121     static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
122     static final String TYPE_VIDEO = "video";
123 
124     static final String TYPE_AUDIO_ROOT = "audio_root";
125     static final String TYPE_AUDIO = "audio";
126     static final String TYPE_ARTIST = "artist";
127     static final String TYPE_ALBUM = "album";
128 
129     static final String TYPE_DOCUMENTS_ROOT = "documents_root";
130     static final String TYPE_DOCUMENTS_BUCKET = "documents_bucket";
131     static final String TYPE_DOCUMENT = "document";
132 
133     private static volatile boolean sMediaStoreReady = false;
134 
135     private static volatile boolean sReturnedImagesEmpty = false;
136     private static volatile boolean sReturnedVideosEmpty = false;
137     private static volatile boolean sReturnedAudioEmpty = false;
138     private static volatile boolean sReturnedDocumentsEmpty = false;
139 
joinNewline(String... args)140     private static String joinNewline(String... args) {
141         return TextUtils.join("\n", args);
142     }
143 
144     public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio";
145     public static final String METADATA_KEY_VIDEO = "android.media.metadata.video";
146 
147     /*
148      * A mapping between media columns and metadata tag names. These keys of the
149      * map form the projection for queries against the media store database.
150      */
151     private static final Map<String, String> IMAGE_COLUMN_MAP = new HashMap<>();
152     private static final Map<String, String> VIDEO_COLUMN_MAP = new HashMap<>();
153     private static final Map<String, String> AUDIO_COLUMN_MAP = new HashMap<>();
154 
155     static {
156         /**
157          * Note that for images (jpegs at least) we'll first try an alternate
158          * means of extracting metadata, one that provides more data. But if
159          * that fails, or if the image type is not JPEG, we fall back to these columns.
160          */
IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH)161         IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH);
IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH)162         IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH);
IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME)163         IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME);
164 
VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION)165         VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION);
VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH)166         VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH);
VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH)167         VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH);
VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE)168         VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE);
169 
AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST)170         AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST);
AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER)171         AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER);
AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM)172         AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM);
AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR)173         AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR);
AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION)174         AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION);
175     }
176 
177     @Override
onCreate()178     public boolean onCreate() {
179         notifyRootsChanged(getContext());
180         return true;
181     }
182 
enforceShellRestrictions()183     private void enforceShellRestrictions() {
184         final int callingAppId = UserHandle.getAppId(Binder.getCallingUid());
185         if (callingAppId == android.os.Process.SHELL_UID
186                 && getContext().getSystemService(UserManager.class)
187                         .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
188             throw new SecurityException(
189                     "Shell user cannot access files for user " + UserHandle.myUserId());
190         }
191     }
192 
notifyRootsChanged(Context context)193     private static void notifyRootsChanged(Context context) {
194         context.getContentResolver()
195                 .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
196     }
197 
198     /**
199      * When underlying provider is ready, we kick off a notification of roots
200      * changed so they can be refreshed.
201      */
onMediaStoreReady(Context context, String volumeName)202     static void onMediaStoreReady(Context context, String volumeName) {
203         sMediaStoreReady = true;
204         notifyRootsChanged(context);
205     }
206 
207     /**
208      * When inserting the first item of each type, we need to trigger a roots
209      * refresh to clear a previously reported {@link Root#FLAG_EMPTY}.
210      */
onMediaStoreInsert(Context context, String volumeName, int type, long id)211     static void onMediaStoreInsert(Context context, String volumeName, int type, long id) {
212         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) return;
213 
214         if (type == FileColumns.MEDIA_TYPE_IMAGE && sReturnedImagesEmpty) {
215             sReturnedImagesEmpty = false;
216             notifyRootsChanged(context);
217         } else if (type == FileColumns.MEDIA_TYPE_VIDEO && sReturnedVideosEmpty) {
218             sReturnedVideosEmpty = false;
219             notifyRootsChanged(context);
220         } else if (type == FileColumns.MEDIA_TYPE_AUDIO && sReturnedAudioEmpty) {
221             sReturnedAudioEmpty = false;
222             notifyRootsChanged(context);
223         } else if (type == FileColumns.MEDIA_TYPE_DOCUMENT && sReturnedDocumentsEmpty) {
224             sReturnedDocumentsEmpty = false;
225             notifyRootsChanged(context);
226         }
227     }
228 
229     /**
230      * When deleting an item, we need to revoke any outstanding Uri grants.
231      */
onMediaStoreDelete(Context context, String volumeName, int type, long id)232     static void onMediaStoreDelete(Context context, String volumeName, int type, long id) {
233         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) return;
234 
235         if (type == FileColumns.MEDIA_TYPE_IMAGE) {
236             final Uri uri = DocumentsContract.buildDocumentUri(
237                     AUTHORITY, getDocIdForIdent(TYPE_IMAGE, id));
238             context.revokeUriPermission(uri, ~0);
239             notifyRootsChanged(context);
240         } else if (type == FileColumns.MEDIA_TYPE_VIDEO) {
241             final Uri uri = DocumentsContract.buildDocumentUri(
242                     AUTHORITY, getDocIdForIdent(TYPE_VIDEO, id));
243             context.revokeUriPermission(uri, ~0);
244             notifyRootsChanged(context);
245         } else if (type == FileColumns.MEDIA_TYPE_AUDIO) {
246             final Uri uri = DocumentsContract.buildDocumentUri(
247                     AUTHORITY, getDocIdForIdent(TYPE_AUDIO, id));
248             context.revokeUriPermission(uri, ~0);
249             notifyRootsChanged(context);
250         } else if (type == FileColumns.MEDIA_TYPE_DOCUMENT) {
251             final Uri uri = DocumentsContract.buildDocumentUri(
252                     AUTHORITY, getDocIdForIdent(TYPE_DOCUMENT, id));
253             context.revokeUriPermission(uri, ~0);
254             notifyRootsChanged(context);
255         }
256     }
257 
258     private static class Ident {
259         public String type;
260         public long id;
261     }
262 
getIdentForDocId(String docId)263     private static Ident getIdentForDocId(String docId) {
264         final Ident ident = new Ident();
265         final int split = docId.indexOf(':');
266         if (split == -1) {
267             ident.type = docId;
268             ident.id = -1;
269         } else {
270             ident.type = docId.substring(0, split);
271             ident.id = Long.parseLong(docId.substring(split + 1));
272         }
273         return ident;
274     }
275 
getDocIdForIdent(String type, long id)276     private static String getDocIdForIdent(String type, long id) {
277         return type + ":" + id;
278     }
279 
resolveRootProjection(String[] projection)280     private static String[] resolveRootProjection(String[] projection) {
281         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
282     }
283 
resolveDocumentProjection(String[] projection)284     private static String[] resolveDocumentProjection(String[] projection) {
285         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
286     }
287 
buildSearchSelection(String displayName, String[] mimeTypes, long lastModifiedAfter, long fileSizeOver, String columnDisplayName, String columnMimeType, String columnLastModified, String columnFileSize)288     static Pair<String, String[]> buildSearchSelection(String displayName,
289             String[] mimeTypes, long lastModifiedAfter, long fileSizeOver, String columnDisplayName,
290             String columnMimeType, String columnLastModified, String columnFileSize) {
291         StringBuilder selection = new StringBuilder();
292         final List<String> selectionArgs = new ArrayList<>();
293 
294         if (!displayName.isEmpty()) {
295             selection.append(columnDisplayName + " LIKE ?");
296             selectionArgs.add("%" + displayName + "%");
297         }
298 
299         if (lastModifiedAfter != -1) {
300             if (selection.length() > 0) {
301                 selection.append(" AND ");
302             }
303 
304             // The units of DATE_MODIFIED are seconds since 1970.
305             // The units of lastModified are milliseconds since 1970.
306             selection.append(columnLastModified + " > " + lastModifiedAfter / 1000);
307         }
308 
309         if (fileSizeOver != -1) {
310             if (selection.length() > 0) {
311                 selection.append(" AND ");
312             }
313 
314             selection.append(columnFileSize + " > " + fileSizeOver);
315         }
316 
317         if (mimeTypes != null && mimeTypes.length > 0) {
318             if (selection.length() > 0) {
319                 selection.append(" AND ");
320             }
321 
322             selection.append("(");
323             final List<String> tempSelectionArgs = new ArrayList<>();
324             final StringBuilder tempSelection = new StringBuilder();
325             List<String> wildcardMimeTypeList = new ArrayList<>();
326             for (int i = 0; i < mimeTypes.length; ++i) {
327                 final String mimeType = mimeTypes[i];
328                 if (!TextUtils.isEmpty(mimeType) && mimeType.endsWith("/*")) {
329                     wildcardMimeTypeList.add(mimeType);
330                     continue;
331                 }
332 
333                 if (tempSelectionArgs.size() > 0) {
334                     tempSelection.append(",");
335                 }
336                 tempSelection.append("?");
337                 tempSelectionArgs.add(mimeType);
338             }
339 
340             for (int i = 0; i < wildcardMimeTypeList.size(); i++) {
341                 selection.append(columnMimeType + " LIKE ?")
342                         .append((i != wildcardMimeTypeList.size() - 1) ? " OR " : "");
343                 final String mimeType = wildcardMimeTypeList.get(i);
344                 selectionArgs.add(mimeType.substring(0, mimeType.length() - 1) + "%");
345             }
346 
347             if (tempSelectionArgs.size() > 0) {
348                 if (wildcardMimeTypeList.size() > 0) {
349                     selection.append(" OR ");
350                 }
351                 selection.append(columnMimeType + " IN (")
352                         .append(tempSelection.toString())
353                         .append(")");
354                 selectionArgs.addAll(tempSelectionArgs);
355             }
356 
357             selection.append(")");
358         }
359 
360         return new Pair<>(selection.toString(), selectionArgs.toArray(new String[0]));
361     }
362 
addDocumentSelection(String selection, String[] selectionArgs)363     static Pair<String, String[]> addDocumentSelection(String selection,
364             String[] selectionArgs) {
365         String retSelection = "";
366         final List<String> retSelectionArgs = new ArrayList<>();
367         if (!TextUtils.isEmpty(selection) && selectionArgs != null) {
368             retSelection = selection + " AND ";
369             for (int i = 0; i < selectionArgs.length; i++) {
370                 retSelectionArgs.add(selectionArgs[i]);
371             }
372         }
373         retSelection += FileColumns.MEDIA_TYPE + "=?";
374         retSelectionArgs.add("" + FileColumns.MEDIA_TYPE_DOCUMENT);
375         return new Pair<>(retSelection, retSelectionArgs.toArray(new String[0]));
376     }
377 
378     /**
379      * Check whether filter mime type and get the matched mime types.
380      * If we don't need to filter mime type, the matchedMimeTypes will be empty.
381      *
382      * @param mimeTypes the mime types to test
383      * @param filter the filter. It is "image/*" or "video/*" or "audio/*".
384      * @param matchedMimeTypes the matched mime types will add into this.
385      * @return true, should do mime type filter. false, no need.
386      */
shouldFilterMimeType(String[] mimeTypes, String filter, List<String> matchedMimeTypes)387     private static boolean shouldFilterMimeType(String[] mimeTypes, String filter,
388             List<String> matchedMimeTypes) {
389         matchedMimeTypes.clear();
390         boolean shouldQueryMimeType = true;
391         if (mimeTypes != null) {
392             for (int i = 0; i < mimeTypes.length; i++) {
393                 // If the mime type is "*/*" or "image/*" or "video/*" or "audio/*",
394                 // we don't need to filter mime type.
395                 if (TextUtils.equals(mimeTypes[i], "*/*") ||
396                         TextUtils.equals(mimeTypes[i], filter)) {
397                     matchedMimeTypes.clear();
398                     shouldQueryMimeType = false;
399                     break;
400                 }
401                 if (MimeTypeFilter.matches(mimeTypes[i], filter)) {
402                     matchedMimeTypes.add(mimeTypes[i]);
403                 }
404             }
405         } else {
406             shouldQueryMimeType = false;
407         }
408 
409         return shouldQueryMimeType;
410     }
411 
getUriForDocumentId(String docId)412     private Uri getUriForDocumentId(String docId) {
413         final Ident ident = getIdentForDocId(docId);
414         if (TYPE_IMAGE.equals(ident.type) && ident.id != -1) {
415             return ContentUris.withAppendedId(
416                     Images.Media.EXTERNAL_CONTENT_URI, ident.id);
417         } else if (TYPE_VIDEO.equals(ident.type) && ident.id != -1) {
418             return ContentUris.withAppendedId(
419                     Video.Media.EXTERNAL_CONTENT_URI, ident.id);
420         } else if (TYPE_AUDIO.equals(ident.type) && ident.id != -1) {
421             return ContentUris.withAppendedId(
422                     Audio.Media.EXTERNAL_CONTENT_URI, ident.id);
423         } else if (TYPE_DOCUMENT.equals(ident.type) && ident.id != -1) {
424             return ContentUris.withAppendedId(
425                     Files.EXTERNAL_CONTENT_URI, ident.id);
426         } else {
427             throw new UnsupportedOperationException("Unsupported document " + docId);
428         }
429     }
430 
431     @Override
deleteDocument(String docId)432     public void deleteDocument(String docId) throws FileNotFoundException {
433         enforceShellRestrictions();
434         final Uri target = getUriForDocumentId(docId);
435 
436         // Delegate to real provider
437         final long token = Binder.clearCallingIdentity();
438         try {
439             getContext().getContentResolver().delete(target, null, null);
440         } finally {
441             Binder.restoreCallingIdentity(token);
442         }
443     }
444 
445     @Override
getDocumentMetadata(String docId)446     public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException {
447         enforceShellRestrictions();
448         return getDocumentMetadataFromIndex(docId);
449     }
450 
getDocumentMetadataFromIndex(String docId)451     public @Nullable Bundle getDocumentMetadataFromIndex(String docId)
452             throws FileNotFoundException {
453 
454         final Ident ident = getIdentForDocId(docId);
455 
456         Map<String, String> columnMap = null;
457         String tagType;
458         Uri query;
459 
460         switch (ident.type) {
461             case TYPE_IMAGE:
462                 columnMap = IMAGE_COLUMN_MAP;
463                 tagType = DocumentsContract.METADATA_EXIF;
464                 query = Images.Media.EXTERNAL_CONTENT_URI;
465                 break;
466             case TYPE_VIDEO:
467                 columnMap = VIDEO_COLUMN_MAP;
468                 tagType = METADATA_KEY_VIDEO;
469                 query = Video.Media.EXTERNAL_CONTENT_URI;
470                 break;
471             case TYPE_AUDIO:
472                 columnMap = AUDIO_COLUMN_MAP;
473                 tagType = METADATA_KEY_AUDIO;
474                 query = Audio.Media.EXTERNAL_CONTENT_URI;
475                 break;
476             default:
477                 // Unsupported file type.
478                 throw new FileNotFoundException(
479                     "Metadata request for unsupported file type: " + ident.type);
480         }
481 
482         final long token = Binder.clearCallingIdentity();
483         Cursor cursor = null;
484         Bundle result = null;
485 
486         final ContentResolver resolver = getContext().getContentResolver();
487         Collection<String> columns = columnMap.keySet();
488         String[] projection = columns.toArray(new String[columns.size()]);
489         try {
490             cursor = resolver.query(
491                     query,
492                     projection,
493                     BaseColumns._ID + "=?",
494                     new String[]{Long.toString(ident.id)},
495                     null);
496 
497             if (!cursor.moveToFirst()) {
498                 throw new FileNotFoundException("Can't find document id: " + docId);
499             }
500 
501             final Bundle metadata = extractMetadataFromCursor(cursor, columnMap);
502             result = new Bundle();
503             result.putBundle(tagType, metadata);
504             result.putStringArray(
505                     DocumentsContract.METADATA_TYPES,
506                     new String[]{tagType});
507         } finally {
508             FileUtils.closeQuietly(cursor);
509             Binder.restoreCallingIdentity(token);
510         }
511         return result;
512     }
513 
extractMetadataFromCursor(Cursor cursor, Map<String, String> columns)514     private static Bundle extractMetadataFromCursor(Cursor cursor, Map<String, String> columns) {
515 
516         assert (cursor.getCount() == 1);
517 
518         final Bundle metadata = new Bundle();
519         for (String col : columns.keySet()) {
520 
521             int index = cursor.getColumnIndex(col);
522             String bundleTag = columns.get(col);
523 
524             // Special case to be able to pull longs out of a cursor, as long is not a supported
525             // field of getType.
526             if (ExifInterface.TAG_DATETIME.equals(bundleTag)) {
527                 if (!cursor.isNull(index)) {
528                     // format string to be consistent with how EXIF interface formats the date.
529                     long date = cursor.getLong(index);
530                     String format = DateFormat.getBestDateTimePattern(Locale.getDefault(),
531                             "MMM dd, yyyy, hh:mm");
532                     metadata.putString(bundleTag, DateFormat.format(format, date).toString());
533                 }
534                 continue;
535             }
536 
537             switch (cursor.getType(index)) {
538                 case Cursor.FIELD_TYPE_INTEGER:
539                     metadata.putInt(bundleTag, cursor.getInt(index));
540                     break;
541                 case Cursor.FIELD_TYPE_FLOAT:
542                     //Errors on the side of greater precision since interface doesnt support doubles
543                     metadata.putFloat(bundleTag, cursor.getFloat(index));
544                     break;
545                 case Cursor.FIELD_TYPE_STRING:
546                     metadata.putString(bundleTag, cursor.getString(index));
547                     break;
548                 case Cursor.FIELD_TYPE_BLOB:
549                     Log.d(TAG, "Unsupported type, blob, for col: " + bundleTag);
550                     break;
551                 case Cursor.FIELD_TYPE_NULL:
552                     Log.d(TAG, "Unsupported type, null, for col: " + bundleTag);
553                     break;
554                 default:
555                     throw new RuntimeException("Data type not supported");
556             }
557         }
558 
559         return metadata;
560     }
561 
562     @Override
queryRoots(String[] projection)563     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
564         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
565         // Skip all roots when the underlying provider isn't ready yet so that
566         // we avoid triggering an ANR; we'll circle back to notify and refresh
567         // once it's ready
568         if (sMediaStoreReady) {
569             includeImagesRoot(result);
570             includeVideosRoot(result);
571             includeAudioRoot(result);
572             includeDocumentsRoot(result);
573         }
574         return result;
575     }
576 
577     @Override
queryDocument(String docId, String[] projection)578     public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
579         enforceShellRestrictions();
580         final ContentResolver resolver = getContext().getContentResolver();
581         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
582         final Ident ident = getIdentForDocId(docId);
583         final String[] queryArgs = new String[] { Long.toString(ident.id) } ;
584 
585         final long token = Binder.clearCallingIdentity();
586         Cursor cursor = null;
587         try {
588             if (TYPE_IMAGES_ROOT.equals(ident.type)) {
589                 // single root
590                 includeImagesRootDocument(result);
591             } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
592                 // single bucket
593                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
594                         ImagesBucketQuery.PROJECTION, ImageColumns.BUCKET_ID + "=?",
595                         queryArgs, ImagesBucketQuery.SORT_ORDER);
596                 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI);
597                 if (cursor.moveToFirst()) {
598                     includeImagesBucket(result, cursor);
599                 }
600             } else if (TYPE_IMAGE.equals(ident.type)) {
601                 // single image
602                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
603                         ImageQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
604                         null);
605                 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI);
606                 if (cursor.moveToFirst()) {
607                     includeImage(result, cursor);
608                 }
609             } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) {
610                 // single root
611                 includeVideosRootDocument(result);
612             } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
613                 // single bucket
614                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
615                         VideosBucketQuery.PROJECTION, VideoColumns.BUCKET_ID + "=?",
616                         queryArgs, VideosBucketQuery.SORT_ORDER);
617                 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI);
618                 if (cursor.moveToFirst()) {
619                     includeVideosBucket(result, cursor);
620                 }
621             } else if (TYPE_VIDEO.equals(ident.type)) {
622                 // single video
623                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
624                         VideoQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
625                         null);
626                 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI);
627                 if (cursor.moveToFirst()) {
628                     includeVideo(result, cursor);
629                 }
630             } else if (TYPE_AUDIO_ROOT.equals(ident.type)) {
631                 // single root
632                 includeAudioRootDocument(result);
633             } else if (TYPE_ARTIST.equals(ident.type)) {
634                 // single artist
635                 cursor = resolver.query(Artists.EXTERNAL_CONTENT_URI,
636                         ArtistQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
637                         null);
638                 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
639                 if (cursor.moveToFirst()) {
640                     includeArtist(result, cursor);
641                 }
642             } else if (TYPE_ALBUM.equals(ident.type)) {
643                 // single album
644                 cursor = resolver.query(Albums.EXTERNAL_CONTENT_URI,
645                         AlbumQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
646                         null);
647                 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
648                 if (cursor.moveToFirst()) {
649                     includeAlbum(result, cursor);
650                 }
651             } else if (TYPE_AUDIO.equals(ident.type)) {
652                 // single song
653                 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI,
654                         SongQuery.PROJECTION, BaseColumns._ID + "=?", queryArgs,
655                         null);
656                 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
657                 if (cursor.moveToFirst()) {
658                     includeAudio(result, cursor);
659                 }
660             }  else if (TYPE_DOCUMENTS_ROOT.equals(ident.type)) {
661                 // single root
662                 includeDocumentsRootDocument(result);
663             } else if (TYPE_DOCUMENTS_BUCKET.equals(ident.type)) {
664                 // single bucket
665                 final Pair<String, String[]> selectionPair = addDocumentSelection(
666                         FileColumns.BUCKET_ID + "=?", queryArgs);
667                 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentsBucketQuery.PROJECTION,
668                         selectionPair.first, selectionPair.second, DocumentsBucketQuery.SORT_ORDER);
669                 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI);
670                 if (cursor.moveToFirst()) {
671                     includeDocumentsBucket(result, cursor);
672                 }
673             } else if (TYPE_DOCUMENT.equals(ident.type)) {
674                 // single document
675                 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION,
676                         FileColumns._ID + "=?", queryArgs, null);
677                 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI);
678                 if (cursor.moveToFirst()) {
679                     includeDocument(result, cursor);
680                 }
681             } else {
682                 throw new UnsupportedOperationException("Unsupported document " + docId);
683             }
684         } finally {
685             FileUtils.closeQuietly(cursor);
686             Binder.restoreCallingIdentity(token);
687         }
688         return result;
689     }
690 
691     @Override
queryChildDocuments(String docId, String[] projection, String sortOrder)692     public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
693             throws FileNotFoundException {
694         enforceShellRestrictions();
695         final ContentResolver resolver = getContext().getContentResolver();
696         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
697         final Ident ident = getIdentForDocId(docId);
698         final String[] queryArgs = new String[] { Long.toString(ident.id) } ;
699 
700         final long token = Binder.clearCallingIdentity();
701         Cursor cursor = null;
702         try {
703             if (TYPE_IMAGES_ROOT.equals(ident.type)) {
704                 // include all unique buckets
705                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
706                         ImagesBucketQuery.PROJECTION, null, null, ImagesBucketQuery.SORT_ORDER);
707                 // multiple orders
708                 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI);
709                 long lastId = Long.MIN_VALUE;
710                 while (cursor.moveToNext()) {
711                     final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID);
712                     if (lastId != id) {
713                         includeImagesBucket(result, cursor);
714                         lastId = id;
715                     }
716                 }
717             } else if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
718                 // include images under bucket
719                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
720                         ImageQuery.PROJECTION, ImageColumns.BUCKET_ID + "=?",
721                         queryArgs, null);
722                 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI);
723                 while (cursor.moveToNext()) {
724                     includeImage(result, cursor);
725                 }
726             } else if (TYPE_VIDEOS_ROOT.equals(ident.type)) {
727                 // include all unique buckets
728                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
729                         VideosBucketQuery.PROJECTION, null, null, VideosBucketQuery.SORT_ORDER);
730                 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI);
731                 long lastId = Long.MIN_VALUE;
732                 while (cursor.moveToNext()) {
733                     final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID);
734                     if (lastId != id) {
735                         includeVideosBucket(result, cursor);
736                         lastId = id;
737                     }
738                 }
739             } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
740                 // include videos under bucket
741                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
742                         VideoQuery.PROJECTION, VideoColumns.BUCKET_ID + "=?",
743                         queryArgs, null);
744                 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI);
745                 while (cursor.moveToNext()) {
746                     includeVideo(result, cursor);
747                 }
748             } else if (TYPE_AUDIO_ROOT.equals(ident.type)) {
749                 // include all artists
750                 cursor = resolver.query(Audio.Artists.EXTERNAL_CONTENT_URI,
751                         ArtistQuery.PROJECTION, null, null, null);
752                 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
753                 while (cursor.moveToNext()) {
754                     includeArtist(result, cursor);
755                 }
756             } else if (TYPE_ARTIST.equals(ident.type)) {
757                 // include all albums under artist
758                 cursor = resolver.query(Artists.Albums.getContentUri("external", ident.id),
759                         AlbumQuery.PROJECTION, null, null, null);
760                 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
761                 while (cursor.moveToNext()) {
762                     includeAlbum(result, cursor);
763                 }
764             } else if (TYPE_ALBUM.equals(ident.type)) {
765                 // include all songs under album
766                 cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI,
767                         SongQuery.PROJECTION, AudioColumns.ALBUM_ID + "=?",
768                         queryArgs, null);
769                 result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
770                 while (cursor.moveToNext()) {
771                     includeAudio(result, cursor);
772                 }
773             } else if (TYPE_DOCUMENTS_ROOT.equals(ident.type)) {
774                 // include all unique buckets
775                 final Pair<String, String[]> selectionPair = addDocumentSelection(null, null);
776                 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentsBucketQuery.PROJECTION,
777                         selectionPair.first, selectionPair.second, DocumentsBucketQuery.SORT_ORDER);
778                 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI);
779                 long lastId = Long.MIN_VALUE;
780                 while (cursor.moveToNext()) {
781                     final long id = cursor.getLong(DocumentsBucketQuery.BUCKET_ID);
782                     if (lastId != id) {
783                         includeDocumentsBucket(result, cursor);
784                         lastId = id;
785                     }
786                 }
787             } else if (TYPE_DOCUMENTS_BUCKET.equals(ident.type)) {
788                 // include documents under bucket
789                 final Pair<String, String[]> selectionPair = addDocumentSelection(
790                         FileColumns.BUCKET_ID + "=?", queryArgs);
791                 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION,
792                         selectionPair.first, selectionPair.second, null);
793                 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI);
794                 while (cursor.moveToNext()) {
795                     includeDocument(result, cursor);
796                 }
797             } else {
798                 throw new UnsupportedOperationException("Unsupported document " + docId);
799             }
800         } finally {
801             FileUtils.closeQuietly(cursor);
802             Binder.restoreCallingIdentity(token);
803         }
804         return result;
805     }
806 
807     @Override
queryRecentDocuments(String rootId, String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)808     public Cursor queryRecentDocuments(String rootId, String[] projection,
809             @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)
810             throws FileNotFoundException {
811         enforceShellRestrictions();
812         final ContentResolver resolver = getContext().getContentResolver();
813         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
814 
815         final long token = Binder.clearCallingIdentity();
816 
817         int limit = -1;
818         if (queryArgs != null) {
819             limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, -1);
820         }
821         if (limit < 0) {
822             // Use default value, and no QUERY_ARG* is honored.
823             limit = 64;
824         } else {
825             // We are honoring the QUERY_ARG_LIMIT.
826             Bundle extras = new Bundle();
827             result.setExtras(extras);
828             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[]{
829                 ContentResolver.QUERY_ARG_LIMIT
830             });
831         }
832 
833         Cursor cursor = null;
834         try {
835             if (TYPE_IMAGES_ROOT.equals(rootId)) {
836                 // include all unique buckets
837                 cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
838                         ImageQuery.PROJECTION, null, null, ImageColumns.DATE_MODIFIED + " DESC");
839                 result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI);
840                 while (cursor.moveToNext() && result.getCount() < limit) {
841                     includeImage(result, cursor);
842                 }
843             } else if (TYPE_VIDEOS_ROOT.equals(rootId)) {
844                 // include all unique buckets
845                 cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
846                         VideoQuery.PROJECTION, null, null, VideoColumns.DATE_MODIFIED + " DESC");
847                 result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI);
848                 while (cursor.moveToNext() && result.getCount() < limit) {
849                     includeVideo(result, cursor);
850                 }
851             } else if (TYPE_DOCUMENTS_ROOT.equals(rootId)) {
852                 // include all unique buckets
853                 final Pair<String, String[]> selectionPair = addDocumentSelection(null, null);
854                 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION,
855                         selectionPair.first, selectionPair.second,
856                         FileColumns.DATE_MODIFIED + " DESC");
857                 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI);
858                 while (cursor.moveToNext() && result.getCount() < limit) {
859                     includeDocument(result, cursor);
860                 }
861             } else {
862                 throw new UnsupportedOperationException("Unsupported root " + rootId);
863             }
864         } finally {
865             FileUtils.closeQuietly(cursor);
866             Binder.restoreCallingIdentity(token);
867         }
868         return result;
869     }
870 
871     @Override
querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)872     public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
873             throws FileNotFoundException {
874         enforceShellRestrictions();
875         final ContentResolver resolver = getContext().getContentResolver();
876         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
877 
878         final long token = Binder.clearCallingIdentity();
879 
880         final String displayName = queryArgs.getString(DocumentsContract.QUERY_ARG_DISPLAY_NAME,
881                 "" /* defaultValue */);
882         final long lastModifiedAfter = queryArgs.getLong(
883                 DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */);
884         final long fileSizeOver = queryArgs.getLong(DocumentsContract.QUERY_ARG_FILE_SIZE_OVER,
885                 -1 /* defaultValue */);
886         final String[] mimeTypes = queryArgs.getStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES);
887         final ArrayList<String> matchedMimeTypes = new ArrayList<>();
888 
889         Cursor cursor = null;
890         try {
891             if (TYPE_IMAGES_ROOT.equals(rootId)) {
892                 final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "image/*",
893                         matchedMimeTypes);
894 
895                 // If the queried mime types didn't match the root, we don't need to
896                 // query the provider. Ex: the queried mime type is "video/*", but the root
897                 // is images root.
898                 if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) {
899                     final Pair<String, String[]> selectionPair = buildSearchSelection(displayName,
900                             matchedMimeTypes.toArray(new String[0]), lastModifiedAfter,
901                             fileSizeOver, ImageColumns.DISPLAY_NAME, ImageColumns.MIME_TYPE,
902                             ImageColumns.DATE_MODIFIED, ImageColumns.SIZE);
903 
904                     cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
905                             ImageQuery.PROJECTION,
906                             selectionPair.first, selectionPair.second,
907                             ImageColumns.DATE_MODIFIED + " DESC");
908 
909                     result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI);
910                     while (cursor.moveToNext()) {
911                         includeImage(result, cursor);
912                     }
913                 }
914             } else if (TYPE_VIDEOS_ROOT.equals(rootId)) {
915                 final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "video/*",
916                         matchedMimeTypes);
917 
918                 // If the queried mime types didn't match the root, we don't need to
919                 // query the provider.
920                 if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) {
921                     final Pair<String, String[]> selectionPair = buildSearchSelection(displayName,
922                             matchedMimeTypes.toArray(new String[0]), lastModifiedAfter,
923                             fileSizeOver, VideoColumns.DISPLAY_NAME, VideoColumns.MIME_TYPE,
924                             VideoColumns.DATE_MODIFIED, VideoColumns.SIZE);
925                     cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, VideoQuery.PROJECTION,
926                             selectionPair.first, selectionPair.second,
927                             VideoColumns.DATE_MODIFIED + " DESC");
928                     result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI);
929                     while (cursor.moveToNext()) {
930                         includeVideo(result, cursor);
931                     }
932                 }
933             } else if (TYPE_AUDIO_ROOT.equals(rootId)) {
934                 final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "audio/*",
935                         matchedMimeTypes);
936 
937                 // If the queried mime types didn't match the root, we don't need to
938                 // query the provider.
939                 if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) {
940                     final Pair<String, String[]> selectionPair = buildSearchSelection(displayName,
941                             matchedMimeTypes.toArray(new String[0]), lastModifiedAfter,
942                             fileSizeOver, AudioColumns.TITLE, AudioColumns.MIME_TYPE,
943                             AudioColumns.DATE_MODIFIED, AudioColumns.SIZE);
944 
945                     cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, SongQuery.PROJECTION,
946                             selectionPair.first, selectionPair.second,
947                             AudioColumns.DATE_MODIFIED + " DESC");
948                     result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI);
949                     while (cursor.moveToNext()) {
950                         includeAudio(result, cursor);
951                     }
952                 }
953             } else if (TYPE_DOCUMENTS_ROOT.equals(rootId)) {
954                 final Pair<String, String[]> initialSelectionPair = buildSearchSelection(
955                         displayName, mimeTypes, lastModifiedAfter, fileSizeOver,
956                         FileColumns.DISPLAY_NAME, FileColumns.MIME_TYPE, FileColumns.DATE_MODIFIED,
957                         FileColumns.SIZE);
958                 final Pair<String, String[]> selectionPair = addDocumentSelection(
959                         initialSelectionPair.first, initialSelectionPair.second);
960 
961                 cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION,
962                         selectionPair.first, selectionPair.second,
963                         FileColumns.DATE_MODIFIED + " DESC");
964                 result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI);
965                 while (cursor.moveToNext()) {
966                     includeDocument(result, cursor);
967                 }
968             } else {
969                 throw new UnsupportedOperationException("Unsupported root " + rootId);
970             }
971         } finally {
972             FileUtils.closeQuietly(cursor);
973             Binder.restoreCallingIdentity(token);
974         }
975 
976         final String[] handledQueryArgs = getHandledQueryArguments(queryArgs);
977         if (handledQueryArgs.length > 0) {
978             final Bundle extras = new Bundle();
979             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs);
980             result.setExtras(extras);
981         }
982 
983         return result;
984     }
985 
getHandledQueryArguments(Bundle queryArgs)986     public static String[] getHandledQueryArguments(Bundle queryArgs) {
987         if (queryArgs == null) {
988             return new String[0];
989         }
990 
991         final ArrayList<String> args = new ArrayList<>();
992 
993         if (queryArgs.keySet().contains(QUERY_ARG_EXCLUDE_MEDIA)) {
994             args.add(QUERY_ARG_EXCLUDE_MEDIA);
995         }
996 
997         if (queryArgs.keySet().contains(QUERY_ARG_DISPLAY_NAME)) {
998             args.add(QUERY_ARG_DISPLAY_NAME);
999         }
1000 
1001         if (queryArgs.keySet().contains(QUERY_ARG_FILE_SIZE_OVER)) {
1002             args.add(QUERY_ARG_FILE_SIZE_OVER);
1003         }
1004 
1005         if (queryArgs.keySet().contains(QUERY_ARG_LAST_MODIFIED_AFTER)) {
1006             args.add(QUERY_ARG_LAST_MODIFIED_AFTER);
1007         }
1008 
1009         if (queryArgs.keySet().contains(QUERY_ARG_MIME_TYPES)) {
1010             args.add(QUERY_ARG_MIME_TYPES);
1011         }
1012         return args.toArray(new String[0]);
1013     }
1014 
1015     @Override
openDocument(String docId, String mode, CancellationSignal signal)1016     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
1017             throws FileNotFoundException {
1018         enforceShellRestrictions();
1019         final Uri target = getUriForDocumentId(docId);
1020 
1021         if (!"r".equals(mode)) {
1022             throw new IllegalArgumentException("Media is read-only");
1023         }
1024 
1025         // Delegate to real provider
1026         final long token = Binder.clearCallingIdentity();
1027         try {
1028             return getContext().getContentResolver().openFileDescriptor(target, mode);
1029         } finally {
1030             Binder.restoreCallingIdentity(token);
1031         }
1032     }
1033 
1034     @Override
openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)1035     public AssetFileDescriptor openDocumentThumbnail(
1036             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
1037         enforceShellRestrictions();
1038         final Ident ident = getIdentForDocId(docId);
1039 
1040         final long token = Binder.clearCallingIdentity();
1041         try {
1042             if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
1043                 final long id = getImageForBucketCleared(ident.id);
1044                 return openOrCreateImageThumbnailCleared(id, sizeHint, signal);
1045             } else if (TYPE_IMAGE.equals(ident.type)) {
1046                 return openOrCreateImageThumbnailCleared(ident.id, sizeHint, signal);
1047             } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
1048                 final long id = getVideoForBucketCleared(ident.id);
1049                 return openOrCreateVideoThumbnailCleared(id, sizeHint, signal);
1050             } else if (TYPE_VIDEO.equals(ident.type)) {
1051                 return openOrCreateVideoThumbnailCleared(ident.id, sizeHint, signal);
1052             } else {
1053                 throw new UnsupportedOperationException("Unsupported document " + docId);
1054             }
1055         } finally {
1056             Binder.restoreCallingIdentity(token);
1057         }
1058     }
1059 
isEmpty(Uri uri)1060     private boolean isEmpty(Uri uri) {
1061         final ContentResolver resolver = getContext().getContentResolver();
1062         final long token = Binder.clearCallingIdentity();
1063         try (Cursor cursor = resolver.query(uri,
1064                 new String[] { "COUNT(_id)" }, null, null, null)) {
1065             if (cursor.moveToFirst()) {
1066                 return cursor.getInt(0) == 0;
1067             } else {
1068                 // No count information means we need to assume empty
1069                 return true;
1070             }
1071         } finally {
1072             Binder.restoreCallingIdentity(token);
1073         }
1074     }
1075 
includeImagesRoot(MatrixCursor result)1076     private void includeImagesRoot(MatrixCursor result) {
1077         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH;
1078         if (isEmpty(Images.Media.EXTERNAL_CONTENT_URI)) {
1079             flags |= Root.FLAG_EMPTY;
1080             sReturnedImagesEmpty = true;
1081         }
1082 
1083         final RowBuilder row = result.newRow();
1084         row.add(Root.COLUMN_ROOT_ID, TYPE_IMAGES_ROOT);
1085         row.add(Root.COLUMN_FLAGS, flags);
1086         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_images));
1087         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT);
1088         row.add(Root.COLUMN_MIME_TYPES, IMAGE_MIME_TYPES);
1089         row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
1090     }
1091 
includeVideosRoot(MatrixCursor result)1092     private void includeVideosRoot(MatrixCursor result) {
1093         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH;
1094         if (isEmpty(Video.Media.EXTERNAL_CONTENT_URI)) {
1095             flags |= Root.FLAG_EMPTY;
1096             sReturnedVideosEmpty = true;
1097         }
1098 
1099         final RowBuilder row = result.newRow();
1100         row.add(Root.COLUMN_ROOT_ID, TYPE_VIDEOS_ROOT);
1101         row.add(Root.COLUMN_FLAGS, flags);
1102         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_videos));
1103         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT);
1104         row.add(Root.COLUMN_MIME_TYPES, VIDEO_MIME_TYPES);
1105         row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
1106     }
1107 
includeAudioRoot(MatrixCursor result)1108     private void includeAudioRoot(MatrixCursor result) {
1109         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH;
1110         if (isEmpty(Audio.Media.EXTERNAL_CONTENT_URI)) {
1111             flags |= Root.FLAG_EMPTY;
1112             sReturnedAudioEmpty = true;
1113         }
1114 
1115         final RowBuilder row = result.newRow();
1116         row.add(Root.COLUMN_ROOT_ID, TYPE_AUDIO_ROOT);
1117         row.add(Root.COLUMN_FLAGS, flags);
1118         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_audio));
1119         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT);
1120         row.add(Root.COLUMN_MIME_TYPES, AUDIO_MIME_TYPES);
1121         row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
1122     }
1123 
includeDocumentsRoot(MatrixCursor result)1124     private void includeDocumentsRoot(MatrixCursor result) {
1125         int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH;
1126         if (isEmpty(Files.EXTERNAL_CONTENT_URI)) {
1127             flags |= Root.FLAG_EMPTY;
1128             sReturnedDocumentsEmpty = true;
1129         }
1130 
1131         final RowBuilder row = result.newRow();
1132         row.add(Root.COLUMN_ROOT_ID, TYPE_DOCUMENTS_ROOT);
1133         row.add(Root.COLUMN_FLAGS, flags);
1134         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_documents));
1135         row.add(Root.COLUMN_DOCUMENT_ID, TYPE_DOCUMENTS_ROOT);
1136         row.add(Root.COLUMN_MIME_TYPES, DOCUMENT_MIME_TYPES);
1137         row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
1138     }
1139 
includeImagesRootDocument(MatrixCursor result)1140     private void includeImagesRootDocument(MatrixCursor result) {
1141         final RowBuilder row = result.newRow();
1142         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT);
1143         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_images));
1144         row.add(Document.COLUMN_FLAGS,
1145                 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
1146         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1147     }
1148 
includeVideosRootDocument(MatrixCursor result)1149     private void includeVideosRootDocument(MatrixCursor result) {
1150         final RowBuilder row = result.newRow();
1151         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT);
1152         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_videos));
1153         row.add(Document.COLUMN_FLAGS,
1154                 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
1155         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1156     }
1157 
includeAudioRootDocument(MatrixCursor result)1158     private void includeAudioRootDocument(MatrixCursor result) {
1159         final RowBuilder row = result.newRow();
1160         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT);
1161         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_audio));
1162         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1163     }
1164 
includeDocumentsRootDocument(MatrixCursor result)1165     private void includeDocumentsRootDocument(MatrixCursor result) {
1166         final RowBuilder row = result.newRow();
1167         row.add(Document.COLUMN_DOCUMENT_ID, TYPE_DOCUMENTS_ROOT);
1168         row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_documents));
1169         row.add(Document.COLUMN_FLAGS,
1170                 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
1171         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1172     }
1173 
1174     private interface ImagesBucketQuery {
1175         final String[] PROJECTION = new String[] {
1176                 ImageColumns.BUCKET_ID,
1177                 ImageColumns.BUCKET_DISPLAY_NAME,
1178                 ImageColumns.DATE_MODIFIED,
1179                 ImageColumns.VOLUME_NAME };
1180         final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED
1181                 + " DESC";
1182 
1183         final int BUCKET_ID = 0;
1184         final int BUCKET_DISPLAY_NAME = 1;
1185         final int DATE_MODIFIED = 2;
1186         final int VOLUME_NAME = 3;
1187     }
1188 
includeImagesBucket(MatrixCursor result, Cursor cursor)1189     private void includeImagesBucket(MatrixCursor result, Cursor cursor) {
1190         final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID);
1191         final String docId = getDocIdForIdent(TYPE_IMAGES_BUCKET, id);
1192 
1193         final RowBuilder row = result.newRow();
1194         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1195         row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
1196                 cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME),
1197                 cursor.getString(ImagesBucketQuery.VOLUME_NAME)));
1198         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1199         row.add(Document.COLUMN_LAST_MODIFIED,
1200                 cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1201         row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID
1202                 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
1203     }
1204 
1205     private interface ImageQuery {
1206         final String[] PROJECTION = new String[] {
1207                 ImageColumns._ID,
1208                 ImageColumns.DISPLAY_NAME,
1209                 ImageColumns.MIME_TYPE,
1210                 ImageColumns.SIZE,
1211                 ImageColumns.DATE_MODIFIED };
1212 
1213         final int _ID = 0;
1214         final int DISPLAY_NAME = 1;
1215         final int MIME_TYPE = 2;
1216         final int SIZE = 3;
1217         final int DATE_MODIFIED = 4;
1218     }
1219 
includeImage(MatrixCursor result, Cursor cursor)1220     private void includeImage(MatrixCursor result, Cursor cursor) {
1221         final long id = cursor.getLong(ImageQuery._ID);
1222         final String docId = getDocIdForIdent(TYPE_IMAGE, id);
1223 
1224         final RowBuilder row = result.newRow();
1225         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1226         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ImageQuery.DISPLAY_NAME));
1227         row.add(Document.COLUMN_SIZE, cursor.getLong(ImageQuery.SIZE));
1228         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(ImageQuery.MIME_TYPE));
1229         row.add(Document.COLUMN_LAST_MODIFIED,
1230                 cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1231         row.add(Document.COLUMN_FLAGS,
1232                 Document.FLAG_SUPPORTS_THUMBNAIL
1233                     | Document.FLAG_SUPPORTS_DELETE
1234                     | Document.FLAG_SUPPORTS_METADATA);
1235     }
1236 
1237     private interface VideosBucketQuery {
1238         final String[] PROJECTION = new String[] {
1239                 VideoColumns.BUCKET_ID,
1240                 VideoColumns.BUCKET_DISPLAY_NAME,
1241                 VideoColumns.DATE_MODIFIED,
1242                 VideoColumns.VOLUME_NAME };
1243         final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED
1244                 + " DESC";
1245 
1246         final int BUCKET_ID = 0;
1247         final int BUCKET_DISPLAY_NAME = 1;
1248         final int DATE_MODIFIED = 2;
1249         final int VOLUME_NAME = 3;
1250     }
1251 
includeVideosBucket(MatrixCursor result, Cursor cursor)1252     private void includeVideosBucket(MatrixCursor result, Cursor cursor) {
1253         final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID);
1254         final String docId = getDocIdForIdent(TYPE_VIDEOS_BUCKET, id);
1255 
1256         final RowBuilder row = result.newRow();
1257         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1258         row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
1259                 cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME),
1260                 cursor.getString(VideosBucketQuery.VOLUME_NAME)));
1261         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1262         row.add(Document.COLUMN_LAST_MODIFIED,
1263                 cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1264         row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID
1265                 | Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
1266     }
1267 
1268     private interface VideoQuery {
1269         final String[] PROJECTION = new String[] {
1270                 VideoColumns._ID,
1271                 VideoColumns.DISPLAY_NAME,
1272                 VideoColumns.MIME_TYPE,
1273                 VideoColumns.SIZE,
1274                 VideoColumns.DATE_MODIFIED };
1275 
1276         final int _ID = 0;
1277         final int DISPLAY_NAME = 1;
1278         final int MIME_TYPE = 2;
1279         final int SIZE = 3;
1280         final int DATE_MODIFIED = 4;
1281     }
1282 
includeVideo(MatrixCursor result, Cursor cursor)1283     private void includeVideo(MatrixCursor result, Cursor cursor) {
1284         final long id = cursor.getLong(VideoQuery._ID);
1285         final String docId = getDocIdForIdent(TYPE_VIDEO, id);
1286 
1287         final RowBuilder row = result.newRow();
1288         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1289         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(VideoQuery.DISPLAY_NAME));
1290         row.add(Document.COLUMN_SIZE, cursor.getLong(VideoQuery.SIZE));
1291         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(VideoQuery.MIME_TYPE));
1292         row.add(Document.COLUMN_LAST_MODIFIED,
1293                 cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1294         row.add(Document.COLUMN_FLAGS,
1295                 Document.FLAG_SUPPORTS_THUMBNAIL
1296                     | Document.FLAG_SUPPORTS_DELETE
1297                     | Document.FLAG_SUPPORTS_METADATA);
1298     }
1299 
1300     private interface DocumentsBucketQuery {
1301         final String[] PROJECTION = new String[] {
1302                 FileColumns.BUCKET_ID,
1303                 FileColumns.BUCKET_DISPLAY_NAME,
1304                 FileColumns.DATE_MODIFIED,
1305                 FileColumns.VOLUME_NAME };
1306         final String SORT_ORDER = FileColumns.BUCKET_ID + ", " + FileColumns.DATE_MODIFIED
1307                 + " DESC";
1308 
1309         final int BUCKET_ID = 0;
1310         final int BUCKET_DISPLAY_NAME = 1;
1311         final int DATE_MODIFIED = 2;
1312         final int VOLUME_NAME = 3;
1313     }
1314 
includeDocumentsBucket(MatrixCursor result, Cursor cursor)1315     private void includeDocumentsBucket(MatrixCursor result, Cursor cursor) {
1316         final long id = cursor.getLong(DocumentsBucketQuery.BUCKET_ID);
1317         final String docId = getDocIdForIdent(TYPE_DOCUMENTS_BUCKET, id);
1318 
1319         final RowBuilder row = result.newRow();
1320         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1321         row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
1322                 cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME),
1323                 cursor.getString(DocumentsBucketQuery.VOLUME_NAME)));
1324         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1325         row.add(Document.COLUMN_LAST_MODIFIED,
1326                 cursor.getLong(DocumentsBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1327         row.add(Document.COLUMN_FLAGS,
1328                 Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
1329     }
1330 
1331     private interface DocumentQuery {
1332         final String[] PROJECTION = new String[] {
1333                 FileColumns._ID,
1334                 FileColumns.DISPLAY_NAME,
1335                 FileColumns.MIME_TYPE,
1336                 FileColumns.SIZE,
1337                 FileColumns.DATE_MODIFIED };
1338 
1339         final int _ID = 0;
1340         final int DISPLAY_NAME = 1;
1341         final int MIME_TYPE = 2;
1342         final int SIZE = 3;
1343         final int DATE_MODIFIED = 4;
1344     }
1345 
includeDocument(MatrixCursor result, Cursor cursor)1346     private void includeDocument(MatrixCursor result, Cursor cursor) {
1347         final long id = cursor.getLong(DocumentQuery._ID);
1348         final String docId = getDocIdForIdent(TYPE_DOCUMENT, id);
1349 
1350         final RowBuilder row = result.newRow();
1351         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1352         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(DocumentQuery.DISPLAY_NAME));
1353         row.add(Document.COLUMN_SIZE, cursor.getLong(DocumentQuery.SIZE));
1354         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(DocumentQuery.MIME_TYPE));
1355         row.add(Document.COLUMN_LAST_MODIFIED,
1356                 cursor.getLong(DocumentQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1357         row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
1358     }
1359 
1360     private interface ArtistQuery {
1361         final String[] PROJECTION = new String[] {
1362                 BaseColumns._ID,
1363                 ArtistColumns.ARTIST };
1364 
1365         final int _ID = 0;
1366         final int ARTIST = 1;
1367     }
1368 
includeArtist(MatrixCursor result, Cursor cursor)1369     private void includeArtist(MatrixCursor result, Cursor cursor) {
1370         final long id = cursor.getLong(ArtistQuery._ID);
1371         final String docId = getDocIdForIdent(TYPE_ARTIST, id);
1372 
1373         final RowBuilder row = result.newRow();
1374         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1375         row.add(Document.COLUMN_DISPLAY_NAME,
1376                 cleanUpMediaDisplayName(cursor.getString(ArtistQuery.ARTIST)));
1377         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1378     }
1379 
1380     private interface AlbumQuery {
1381         final String[] PROJECTION = new String[] {
1382                 AlbumColumns.ALBUM_ID,
1383                 AlbumColumns.ALBUM };
1384 
1385         final int ALBUM_ID = 0;
1386         final int ALBUM = 1;
1387     }
1388 
includeAlbum(MatrixCursor result, Cursor cursor)1389     private void includeAlbum(MatrixCursor result, Cursor cursor) {
1390         final long id = cursor.getLong(AlbumQuery.ALBUM_ID);
1391         final String docId = getDocIdForIdent(TYPE_ALBUM, id);
1392 
1393         final RowBuilder row = result.newRow();
1394         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1395         row.add(Document.COLUMN_DISPLAY_NAME,
1396                 cleanUpMediaDisplayName(cursor.getString(AlbumQuery.ALBUM)));
1397         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
1398     }
1399 
1400     private interface SongQuery {
1401         final String[] PROJECTION = new String[] {
1402                 AudioColumns._ID,
1403                 AudioColumns.DISPLAY_NAME,
1404                 AudioColumns.MIME_TYPE,
1405                 AudioColumns.SIZE,
1406                 AudioColumns.DATE_MODIFIED };
1407 
1408         final int _ID = 0;
1409         final int DISPLAY_NAME = 1;
1410         final int MIME_TYPE = 2;
1411         final int SIZE = 3;
1412         final int DATE_MODIFIED = 4;
1413     }
1414 
includeAudio(MatrixCursor result, Cursor cursor)1415     private void includeAudio(MatrixCursor result, Cursor cursor) {
1416         final long id = cursor.getLong(SongQuery._ID);
1417         final String docId = getDocIdForIdent(TYPE_AUDIO, id);
1418 
1419         final RowBuilder row = result.newRow();
1420         row.add(Document.COLUMN_DOCUMENT_ID, docId);
1421         row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(SongQuery.DISPLAY_NAME));
1422         row.add(Document.COLUMN_SIZE, cursor.getLong(SongQuery.SIZE));
1423         row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE));
1424         row.add(Document.COLUMN_LAST_MODIFIED,
1425                 cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
1426         row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE
1427                 | Document.FLAG_SUPPORTS_METADATA);
1428     }
1429 
1430     private interface ImagesBucketThumbnailQuery {
1431         final String[] PROJECTION = new String[] {
1432                 ImageColumns._ID,
1433                 ImageColumns.BUCKET_ID,
1434                 ImageColumns.DATE_MODIFIED };
1435 
1436         final int _ID = 0;
1437         final int BUCKET_ID = 1;
1438         final int DATE_MODIFIED = 2;
1439     }
1440 
getImageForBucketCleared(long bucketId)1441     private long getImageForBucketCleared(long bucketId) throws FileNotFoundException {
1442         final ContentResolver resolver = getContext().getContentResolver();
1443         Cursor cursor = null;
1444         try {
1445             cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
1446                     ImagesBucketThumbnailQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + bucketId,
1447                     null, ImageColumns.DATE_MODIFIED + " DESC");
1448             if (cursor.moveToFirst()) {
1449                 return cursor.getLong(ImagesBucketThumbnailQuery._ID);
1450             }
1451         } finally {
1452             FileUtils.closeQuietly(cursor);
1453         }
1454         throw new FileNotFoundException("No video found for bucket");
1455     }
1456 
openOrCreateImageThumbnailCleared(long id, Point size, CancellationSignal signal)1457     private AssetFileDescriptor openOrCreateImageThumbnailCleared(long id, Point size,
1458             CancellationSignal signal) throws FileNotFoundException {
1459         final Bundle opts = new Bundle();
1460         opts.putParcelable(EXTRA_SIZE, size);
1461 
1462         final Uri uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id);
1463         return getContext().getContentResolver().openTypedAssetFile(uri, "image/*", opts, signal);
1464     }
1465 
1466     private interface VideosBucketThumbnailQuery {
1467         final String[] PROJECTION = new String[] {
1468                 VideoColumns._ID,
1469                 VideoColumns.BUCKET_ID,
1470                 VideoColumns.DATE_MODIFIED };
1471 
1472         final int _ID = 0;
1473         final int BUCKET_ID = 1;
1474         final int DATE_MODIFIED = 2;
1475     }
1476 
getVideoForBucketCleared(long bucketId)1477     private long getVideoForBucketCleared(long bucketId)
1478             throws FileNotFoundException {
1479         final ContentResolver resolver = getContext().getContentResolver();
1480         Cursor cursor = null;
1481         try {
1482             cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
1483                     VideosBucketThumbnailQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + bucketId,
1484                     null, VideoColumns.DATE_MODIFIED + " DESC");
1485             if (cursor.moveToFirst()) {
1486                 return cursor.getLong(VideosBucketThumbnailQuery._ID);
1487             }
1488         } finally {
1489             FileUtils.closeQuietly(cursor);
1490         }
1491         throw new FileNotFoundException("No video found for bucket");
1492     }
1493 
openOrCreateVideoThumbnailCleared(long id, Point size, CancellationSignal signal)1494     private AssetFileDescriptor openOrCreateVideoThumbnailCleared(long id, Point size,
1495             CancellationSignal signal) throws FileNotFoundException {
1496         final Bundle opts = new Bundle();
1497         opts.putParcelable(EXTRA_SIZE, size);
1498 
1499         final Uri uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id);
1500         return getContext().getContentResolver().openTypedAssetFile(uri, "image/*", opts, signal);
1501     }
1502 
cleanUpMediaDisplayName(String displayName)1503     private String cleanUpMediaDisplayName(String displayName) {
1504         if (!MediaStore.UNKNOWN_STRING.equals(displayName)) {
1505             return displayName;
1506         }
1507         return getContext().getResources().getString(R.string.unknown);
1508     }
1509 
cleanUpMediaBucketName(String bucketDisplayName, String volumeName)1510     private String cleanUpMediaBucketName(String bucketDisplayName, String volumeName) {
1511         if (!TextUtils.isEmpty(bucketDisplayName)) {
1512             return bucketDisplayName;
1513         } else if (!Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
1514             return volumeName;
1515         } else {
1516             return getContext().getResources().getString(R.string.unknown);
1517         }
1518     }
1519 }
1520