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