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