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