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