1 /*
2  * Copyright (C) 2021 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.photopicker.data;
18 
19 import static android.content.ContentResolver.EXTRA_HONORED_ARGS;
20 import static android.provider.CloudMediaProviderContract.AlbumColumns;
21 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
22 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
23 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
24 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
25 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
26 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE;
27 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
28 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
29 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
30 
31 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.INT_DEFAULT;
32 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
33 import static com.android.providers.media.photopicker.data.PickerDbFacade.addMimeTypesToQueryBuilderAndSelectionArgs;
34 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
35 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
36 import static com.android.providers.media.util.DatabaseUtils.bindList;
37 
38 import android.content.ContentValues;
39 import android.content.Context;
40 import android.database.Cursor;
41 import android.database.MatrixCursor;
42 import android.database.sqlite.SQLiteConstraintException;
43 import android.database.sqlite.SQLiteDatabase;
44 import android.database.sqlite.SQLiteQueryBuilder;
45 import android.os.Bundle;
46 import android.os.Environment;
47 import android.provider.CloudMediaProviderContract;
48 import android.provider.MediaStore;
49 import android.provider.MediaStore.Files.FileColumns;
50 import android.provider.MediaStore.MediaColumns;
51 import android.text.TextUtils;
52 import android.util.Log;
53 
54 import androidx.annotation.VisibleForTesting;
55 
56 import com.android.providers.media.DatabaseHelper;
57 import com.android.providers.media.VolumeCache;
58 import com.android.providers.media.photopicker.PickerSyncController;
59 import com.android.providers.media.util.MimeUtils;
60 
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.List;
64 
65 /**
66  * This is a facade that hides the complexities of executing some SQL statements on the external db.
67  * It does not do any caller permission checks and is only intended for internal use within the
68  * MediaProvider for the Photo Picker.
69  */
70 public class ExternalDbFacade {
71     private static final String TAG = "ExternalDbFacade";
72     @VisibleForTesting
73     static final String TABLE_FILES = "files";
74 
75     @VisibleForTesting
76     static final String TABLE_DELETED_MEDIA = "deleted_media";
77     @VisibleForTesting
78     static final String COLUMN_OLD_ID = "old_id";
79     private static final String COLUMN_OLD_ID_AS_ID = COLUMN_OLD_ID + " AS " +
80             CloudMediaProviderContract.MediaColumns.ID;
81     private static final String COLUMN_GENERATION_MODIFIED = MediaColumns.GENERATION_MODIFIED;
82 
83     private static final String[] PROJECTION_MEDIA_COLUMNS = new String[] {
84         MediaColumns._ID + " AS " + CloudMediaProviderContract.MediaColumns.ID,
85         "COALESCE(" + MediaColumns.DATE_TAKEN + "," + MediaColumns.DATE_MODIFIED +
86                     "* 1000) AS " + CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS,
87         MediaColumns.GENERATION_MODIFIED + " AS " +
88                 CloudMediaProviderContract.MediaColumns.SYNC_GENERATION,
89         MediaColumns.SIZE + " AS " + CloudMediaProviderContract.MediaColumns.SIZE_BYTES,
90         MediaColumns.MIME_TYPE + " AS " + CloudMediaProviderContract.MediaColumns.MIME_TYPE,
91         FileColumns._SPECIAL_FORMAT + " AS " +
92                 CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION,
93         MediaColumns.DURATION + " AS " + CloudMediaProviderContract.MediaColumns.DURATION_MILLIS,
94         MediaColumns.IS_FAVORITE + " AS " + CloudMediaProviderContract.MediaColumns.IS_FAVORITE,
95         MediaColumns.WIDTH + " AS " + CloudMediaProviderContract.MediaColumns.WIDTH,
96         MediaColumns.HEIGHT + " AS " + CloudMediaProviderContract.MediaColumns.HEIGHT,
97         MediaColumns.ORIENTATION + " AS " + CloudMediaProviderContract.MediaColumns.ORIENTATION,
98     };
99     private static final String[] PROJECTION_MEDIA_INFO = new String[] {
100         "MAX(" + MediaColumns.GENERATION_MODIFIED + ") AS "
101         + MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION
102     };
103     private static final String[] PROJECTION_ALBUM_DB = new String[] {
104         "COUNT(" + MediaColumns._ID + ") AS " + CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT,
105         "MAX(COALESCE(" + MediaColumns.DATE_TAKEN + "," + MediaColumns.DATE_MODIFIED +
106                     "* 1000)) AS " + CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MILLIS,
107         MediaColumns._ID + " AS " + CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID,
108     };
109 
110     private static final String WHERE_IMAGE_TYPE = FileColumns.MEDIA_TYPE + " = "
111             + FileColumns.MEDIA_TYPE_IMAGE;
112     private static final String WHERE_VIDEO_TYPE = FileColumns.MEDIA_TYPE + " = "
113             + FileColumns.MEDIA_TYPE_VIDEO;
114     private static final String WHERE_MEDIA_TYPE = WHERE_IMAGE_TYPE + " OR " + WHERE_VIDEO_TYPE;
115     private static final String WHERE_IS_DOWNLOAD = MediaColumns.IS_DOWNLOAD + " = 1";
116     private static final String WHERE_NOT_TRASHED = MediaColumns.IS_TRASHED + " = 0";
117     private static final String WHERE_NOT_PENDING = MediaColumns.IS_PENDING + " = 0";
118     private static final String WHERE_GREATER_GENERATION =
119             MediaColumns.GENERATION_MODIFIED + " > ?";
120     private static final String WHERE_RELATIVE_PATH = MediaStore.MediaColumns.RELATIVE_PATH
121             + " LIKE ?";
122 
123     private static final String WHERE_DATE_TAKEN_MILLIS_BEFORE =
124             String.format("(%s < CAST(? AS INT) OR (%s = CAST(? AS INT) AND %s < CAST(? AS INT)))",
125                     CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS,
126                     CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS,
127                     MediaColumns._ID);
128 
129 
130     /* Include any directory named exactly {@link Environment.DIRECTORY_SCREENSHOTS}
131      * and its child directories. */
132     private static final String WHERE_RELATIVE_PATH_IS_SCREENSHOT_DIR =
133             MediaStore.MediaColumns.RELATIVE_PATH
134                     + " LIKE '%/"
135                     + Environment.DIRECTORY_SCREENSHOTS
136                     + "/%' OR "
137                     + MediaStore.MediaColumns.RELATIVE_PATH
138                     + " LIKE '"
139                     + Environment.DIRECTORY_SCREENSHOTS
140                     + "/%'";
141 
142     private static final String WHERE_VOLUME_IN_PREFIX =
143             MediaStore.MediaColumns.VOLUME_NAME + " IN %s";
144 
145     public static final String RELATIVE_PATH_CAMERA = Environment.DIRECTORY_DCIM + "/Camera/%";
146 
147     @VisibleForTesting
148     static String[] LOCAL_ALBUM_IDS = {
149         ALBUM_ID_CAMERA,
150         ALBUM_ID_SCREENSHOTS,
151         ALBUM_ID_DOWNLOADS
152     };
153 
154     private final Context mContext;
155     private final DatabaseHelper mDatabaseHelper;
156     private final VolumeCache mVolumeCache;
157 
ExternalDbFacade(Context context, DatabaseHelper databaseHelper, VolumeCache volumeCache)158     public ExternalDbFacade(Context context, DatabaseHelper databaseHelper,
159             VolumeCache volumeCache) {
160         mContext = context;
161         mDatabaseHelper = databaseHelper;
162         mVolumeCache = volumeCache;
163     }
164 
165     /**
166      * Returns {@code true} if the PhotoPicker should be notified of this change, {@code false}
167      * otherwise
168      */
onFileInserted(int mediaType, boolean isPending)169     public boolean onFileInserted(int mediaType, boolean isPending) {
170         if (!mDatabaseHelper.isExternal()) {
171             return false;
172         }
173 
174         return !isPending && MimeUtils.isImageOrVideoMediaType(mediaType);
175     }
176 
177     /**
178      * Adds or removes media to the deleted_media tables
179      *
180      * Returns {@code true} if the PhotoPicker should be notified of this change, {@code false}
181      * otherwise
182      */
onFileUpdated(long oldId, int oldMediaType, int newMediaType, boolean oldIsTrashed, boolean newIsTrashed, boolean oldIsPending, boolean newIsPending, boolean oldIsFavorite, boolean newIsFavorite, int oldSpecialFormat, int newSpecialFormat)183     public boolean onFileUpdated(long oldId, int oldMediaType, int newMediaType,
184             boolean oldIsTrashed, boolean newIsTrashed, boolean oldIsPending,
185             boolean newIsPending, boolean oldIsFavorite, boolean newIsFavorite,
186             int oldSpecialFormat, int newSpecialFormat) {
187         if (!mDatabaseHelper.isExternal()) {
188             return false;
189         }
190 
191         final boolean oldIsMedia= MimeUtils.isImageOrVideoMediaType(oldMediaType);
192         final boolean newIsMedia = MimeUtils.isImageOrVideoMediaType(newMediaType);
193 
194         final boolean oldIsVisible = !oldIsTrashed && !oldIsPending;
195         final boolean newIsVisible = !newIsTrashed && !newIsPending;
196 
197         final boolean oldIsVisibleMedia = oldIsVisible && oldIsMedia;
198         final boolean newIsVisibleMedia = newIsVisible && newIsMedia;
199 
200         if (!oldIsVisibleMedia && newIsVisibleMedia) {
201             // Was not visible media and is now visible media
202             removeDeletedMedia(oldId);
203             return true;
204         } else if (oldIsVisibleMedia && !newIsVisibleMedia) {
205             // Was visible media and is now not visible media
206             addDeletedMedia(oldId);
207             return true;
208         }
209 
210         if (newIsVisibleMedia) {
211             return (oldIsFavorite != newIsFavorite) || (oldSpecialFormat != newSpecialFormat);
212         }
213 
214 
215         // Do nothing, not an interesting change
216         return false;
217     }
218 
219     /**
220      * Adds or removes media to the deleted_media tables
221      *
222      * Returns {@code true} if the PhotoPicker should be notified of this change, {@code false}
223      * otherwise
224      */
onFileDeleted(long id, int mediaType)225     public boolean onFileDeleted(long id, int mediaType) {
226         if (!mDatabaseHelper.isExternal()) {
227             return false;
228         }
229         if (!MimeUtils.isImageOrVideoMediaType(mediaType)) {
230             return false;
231         }
232 
233         addDeletedMedia(id);
234         return true;
235     }
236 
237     /**
238      * Adds media with row id {@code oldId} to the deleted_media table. Returns {@code true} if
239      * if it was successfully added, {@code false} otherwise.
240      */
241     @VisibleForTesting
addDeletedMedia(long oldId)242     boolean addDeletedMedia(long oldId) {
243         return mDatabaseHelper.runWithTransaction((db) -> {
244             SQLiteQueryBuilder qb = createDeletedMediaQueryBuilder();
245 
246             ContentValues cv = new ContentValues();
247             cv.put(COLUMN_OLD_ID, oldId);
248             cv.put(COLUMN_GENERATION_MODIFIED, DatabaseHelper.getGeneration(db));
249 
250             try {
251                 return qb.insert(db, cv) > 0;
252             } catch (SQLiteConstraintException e) {
253                 String select = COLUMN_OLD_ID + " = ?";
254                 String[] selectionArgs = new String[] {String.valueOf(oldId)};
255 
256                 return qb.update(db, cv, select, selectionArgs) > 0;
257             }
258          });
259     }
260 
261     /**
262      * Removes media with row id {@code oldId} from the deleted_media table. Returns {@code true} if
263      * it was successfully removed, {@code false} otherwise.
264      */
265     @VisibleForTesting
removeDeletedMedia(long oldId)266     boolean removeDeletedMedia(long oldId) {
267         return mDatabaseHelper.runWithTransaction(db -> {
268             SQLiteQueryBuilder qb = createDeletedMediaQueryBuilder();
269 
270             return qb.delete(db, COLUMN_OLD_ID + " = ?", new String[] {String.valueOf(oldId)}) > 0;
271          });
272     }
273 
274     /**
275      * Returns all items from the deleted_media table.
276      */
277     public Cursor queryDeletedMedia(long generation) {
278         final Cursor cursor = mDatabaseHelper.runWithTransaction(db -> {
279             SQLiteQueryBuilder qb = createDeletedMediaQueryBuilder();
280             String[] projection = new String[] {COLUMN_OLD_ID_AS_ID};
281             String select = COLUMN_GENERATION_MODIFIED + " > ?";
282             String[] selectionArgs = new String[] {String.valueOf(generation)};
283 
284             return qb.query(db, projection, select, selectionArgs,  /* groupBy */ null,
285                     /* having */ null, /* orderBy */ null);
286          });
287 
288         cursor.setExtras(getCursorExtras(generation, /* albumId */ null, /*pageSize*/ -1,
289                 /*pageToken*/ null));
290         return cursor;
291     }
292 
293     /**
294      * Returns all items from the files table where {@link MediaColumns#GENERATION_MODIFIED}
295      * is greater than {@code generation}.
296      */
297     public Cursor queryMedia(long generation, String albumId, String[] mimeTypes, int pageSize,
298             String pageToken) {
299         final List<String> selectionArgs = new ArrayList<>();
300         final String orderBy = getOrderByClause();
301 
302         Log.d(TAG, "Token received for queryMedia = " + pageToken);
303 
304         final Cursor cursor = mDatabaseHelper.runWithTransaction(db -> {
305             SQLiteQueryBuilder qb = createMediaQueryBuilder();
306             qb.appendWhereStandalone(WHERE_GREATER_GENERATION);
307             selectionArgs.add(String.valueOf(generation));
308 
309             if (pageToken != null) {
310                 String[] lastMedia = parsePageToken(pageToken);
311                 if (lastMedia != null) {
312                     qb.appendWhereStandalone(getDateTakenWhereClause());
313                     addSelectionArgsForWhereClause(lastMedia, selectionArgs);
314                 }
315             }
316 
317             selectionArgs.addAll(appendWhere(qb, albumId, mimeTypes));
318 
319             return qb.query(db, PROJECTION_MEDIA_COLUMNS, /* select */ null,
320                     selectionArgs.toArray(new String[selectionArgs.size()]), /* groupBy */ null,
321                     /* having */ null, orderBy, String.valueOf(pageSize));
322         });
323 
324         String nextPageToken = null;
325         if (cursor.getCount() > 0 && pageSize != INT_DEFAULT) {
326             nextPageToken = setPageToken(cursor);
327 
328         }
329         cursor.setExtras(getCursorExtras(generation, albumId, pageSize, nextPageToken));
330         return cursor;
331     }
332 
333     private static void addSelectionArgsForWhereClause(String[] lastMedia,
334             List<String> selectionArgs) {
335         selectionArgs.add(lastMedia[0]);
336         selectionArgs.add(lastMedia[0]);
337         selectionArgs.add(lastMedia[1]);
338     }
339 
340     private static String[] parsePageToken(String pageToken) {
341         String[] lastMedia = pageToken.split("\\|");
342 
343         if (lastMedia.length != 2) {
344             Log.w(TAG, "Error parsing token in queryMedia.");
345             return null;
346         }
347         return lastMedia;
348     }
349 
350     private static String getDateTakenWhereClause() {
351         return CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS + " IS NOT NULL AND "
352                 + WHERE_DATE_TAKEN_MILLIS_BEFORE;
353     }
354 
355     private static String getOrderByClause() {
356         return CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS + " DESC,"
357                 + CloudMediaProviderContract.MediaColumns.ID + " DESC";
358     }
359 
360 
361     private String setPageToken(Cursor mediaList) {
362         String token = null;
363         if (mediaList.moveToLast()) {
364             String timeTakenMillis = getCursorString(mediaList,
365                     CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS);
366             String lastItemRowId = getCursorString(mediaList,
367                     CloudMediaProviderContract.MediaColumns.ID);
368             token = timeTakenMillis + "|" + lastItemRowId;
369             mediaList.moveToFirst();
370         }
371         return token;
372     }
373 
374     private Bundle getCursorExtras(long generation, String albumId, int pageSize,
375             String pageToken) {
376         final Bundle bundle = new Bundle();
377         final ArrayList<String> honoredArgs = new ArrayList<>();
378 
379         if (generation > LONG_DEFAULT) {
380             honoredArgs.add(EXTRA_SYNC_GENERATION);
381         }
382         if (!TextUtils.isEmpty(albumId)) {
383             honoredArgs.add(EXTRA_ALBUM_ID);
384         }
385 
386         if (pageSize > INT_DEFAULT) {
387             honoredArgs.add(EXTRA_PAGE_SIZE);
388         }
389 
390         if (pageToken != null) {
391             honoredArgs.add(EXTRA_PAGE_TOKEN);
392         }
393 
394         bundle.putString(EXTRA_MEDIA_COLLECTION_ID, getMediaCollectionId());
395         if (pageToken != null) {
396             bundle.putString(EXTRA_PAGE_TOKEN, pageToken);
397         }
398         bundle.putStringArrayList(EXTRA_HONORED_ARGS, honoredArgs);
399 
400         return bundle;
401     }
402 
403     /**
404      * Returns the total count and max {@link MediaColumns#GENERATION_MODIFIED} value
405      * of the media items in the files table greater than {@code generation}.
406      */
407     private Cursor getMediaCollectionInfoCursor(long generation) {
408         final String[] selectionArgs = new String[] {String.valueOf(generation)};
409         final String[] projection = new String[] {
410             MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION
411         };
412 
413         return mDatabaseHelper.runWithTransaction(db -> {
414                 SQLiteQueryBuilder qbMedia = createMediaQueryBuilder();
415                 qbMedia.appendWhereStandalone(WHERE_GREATER_GENERATION);
416                 SQLiteQueryBuilder qbDeletedMedia = createDeletedMediaQueryBuilder();
417                 qbDeletedMedia.appendWhereStandalone(WHERE_GREATER_GENERATION);
418 
419                 try (Cursor mediaCursor = query(qbMedia, db, PROJECTION_MEDIA_INFO, selectionArgs);
420                         Cursor deletedMediaCursor = query(qbDeletedMedia, db,
421                                 PROJECTION_MEDIA_INFO, selectionArgs)) {
422                     final int mediaGenerationIndex = mediaCursor.getColumnIndexOrThrow(
423                             MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION);
424                     final int deletedMediaGenerationIndex =
425                             deletedMediaCursor.getColumnIndexOrThrow(
426                                     MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION);
427 
428                     long mediaGeneration = 0;
429                     if (mediaCursor.moveToFirst()) {
430                         mediaGeneration = mediaCursor.getLong(mediaGenerationIndex);
431                     }
432 
433                     long deletedMediaGeneration = 0;
434                     if (deletedMediaCursor.moveToFirst()) {
435                         deletedMediaGeneration = deletedMediaCursor.getLong(
436                                 deletedMediaGenerationIndex);
437                     }
438 
439                     long maxGeneration = Math.max(mediaGeneration, deletedMediaGeneration);
440                     MatrixCursor result = new MatrixCursor(projection);
441                     result.addRow(new Long[] { maxGeneration });
442 
443                     return result;
444                 }
445             });
446     }
447 
448     public Bundle getMediaCollectionInfo(long generation) {
449         final Bundle bundle = new Bundle();
450         try (Cursor cursor = getMediaCollectionInfoCursor(generation)) {
451             if (cursor.moveToFirst()) {
452                 int generationIndex = cursor.getColumnIndexOrThrow(
453                         MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION);
454 
455                 bundle.putString(MediaCollectionInfo.MEDIA_COLLECTION_ID, getMediaCollectionId());
456                 bundle.putLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION,
457                         cursor.getLong(generationIndex));
458             }
459         }
460         return bundle;
461     }
462 
463     /**
464      * Returns the media item categories from the files table.
465      * Categories are determined with the {@link #LOCAL_ALBUM_IDS}.
466      * If there are no media items under an albumId, the album is skipped from the results.
467      */
468     public Cursor queryAlbums(String[] mimeTypes) {
469         final MatrixCursor c = new MatrixCursor(AlbumColumns.ALL_PROJECTION);
470 
471         for (String albumId: LOCAL_ALBUM_IDS) {
472             Cursor cursor = mDatabaseHelper.runWithTransaction(db -> {
473                 final SQLiteQueryBuilder qb = createMediaQueryBuilder();
474                 final List<String> selectionArgs = new ArrayList<>();
475                 selectionArgs.addAll(appendWhere(qb, albumId, mimeTypes));
476 
477                 return qb.query(db, PROJECTION_ALBUM_DB, /* selection */ null,
478                         selectionArgs.toArray(new String[selectionArgs.size()]), /* groupBy */ null,
479                         /* having */ null, /* orderBy */ null);
480             });
481 
482             if (cursor == null || !cursor.moveToFirst()) {
483                 continue;
484             }
485 
486             long count = getCursorLong(cursor, CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT);
487             if (count == 0) {
488                 continue;
489             }
490 
491             final String[] projectionValue = new String[] {
492                 /* albumId */ albumId,
493                 getCursorString(cursor, AlbumColumns.DATE_TAKEN_MILLIS),
494                 /* displayName */ albumId,
495                 getCursorString(cursor, AlbumColumns.MEDIA_COVER_ID),
496                 String.valueOf(count),
497                 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
498             };
499 
500             c.addRow(projectionValue);
501         }
502 
503         return c;
504     }
505 
506     private static Cursor query(SQLiteQueryBuilder qb, SQLiteDatabase db, String[] projection,
507             String[] selectionArgs) {
508         return qb.query(db, projection, /* select */ null, selectionArgs,
509                 /* groupBy */ null, /* having */ null, /* orderBy */ null);
510     }
511 
512     private static List<String> appendWhere(SQLiteQueryBuilder qb, String albumId,
513             String[] mimeTypes) {
514         final List<String> selectionArgs = new ArrayList<>();
515 
516         addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectionArgs, mimeTypes);
517 
518         if (albumId == null) {
519             return selectionArgs;
520         }
521 
522         switch (albumId) {
523             case ALBUM_ID_CAMERA:
524                 qb.appendWhereStandalone(WHERE_RELATIVE_PATH);
525                 selectionArgs.add(RELATIVE_PATH_CAMERA);
526                 break;
527             case ALBUM_ID_SCREENSHOTS:
528                 qb.appendWhereStandalone(WHERE_RELATIVE_PATH_IS_SCREENSHOT_DIR);
529                 break;
530             case ALBUM_ID_DOWNLOADS:
531                 qb.appendWhereStandalone(WHERE_IS_DOWNLOAD);
532                 break;
533             default:
534                 Log.w(TAG, "No match for album: " + albumId);
535                 break;
536         }
537 
538         return selectionArgs;
539     }
540 
541     private static SQLiteQueryBuilder createDeletedMediaQueryBuilder() {
542         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
543         qb.setTables(TABLE_DELETED_MEDIA);
544 
545         return qb;
546     }
547 
548     private SQLiteQueryBuilder createMediaQueryBuilder() {
549         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
550         qb.setTables(TABLE_FILES);
551         qb.appendWhereStandalone(WHERE_MEDIA_TYPE);
552         qb.appendWhereStandalone(WHERE_NOT_TRASHED);
553         qb.appendWhereStandalone(WHERE_NOT_PENDING);
554 
555         // the file is corrupted if both datetaken and takenmodified are null.
556         // hence exclude those files.
557         qb.appendWhereStandalone(getDateTakenOrDateModifiedNonNull());
558 
559         String[] volumes = getVolumeList();
560         if (volumes.length > 0) {
561             qb.appendWhereStandalone(buildWhereVolumeIn(volumes));
562         }
563 
564         return qb;
565     }
566 
567     private CharSequence getDateTakenOrDateModifiedNonNull() {
568         return MediaColumns.DATE_TAKEN + " IS NOT NULL OR "
569                 + MediaColumns.DATE_MODIFIED + " IS NOT NULL";
570     }
571 
572     private String buildWhereVolumeIn(String[] volumes) {
573         return String.format(WHERE_VOLUME_IN_PREFIX, bindList((Object[]) volumes));
574     }
575 
576     private String[] getVolumeList() {
577         String[] volumeNames = mVolumeCache.getExternalVolumeNames().toArray(new String[0]);
578         Arrays.sort(volumeNames);
579 
580         return volumeNames;
581     }
582 
583     private String getMediaCollectionId() {
584         final String[] volumes = getVolumeList();
585         if (volumes.length == 0) {
586             return MediaStore.getVersion(mContext);
587         }
588 
589         return MediaStore.getVersion(mContext) + ":" + TextUtils.join(":", getVolumeList());
590     }
591 }
592