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; 18 19 import static android.provider.CloudMediaProviderContract.AlbumColumns; 20 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID; 21 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE; 22 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION; 23 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo; 24 import static android.provider.CloudMediaProviderContract.MediaColumns; 25 26 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT; 27 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_ARRAY_DEFAULT; 28 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT; 29 30 import android.content.ContentResolver; 31 import android.content.Intent; 32 import android.database.Cursor; 33 import android.database.MatrixCursor; 34 import android.os.Bundle; 35 import android.os.SystemClock; 36 import android.provider.CloudMediaProvider; 37 import android.provider.CloudMediaProviderContract; 38 import android.text.TextUtils; 39 40 import com.android.providers.media.photopicker.LocalProvider; 41 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 48 /** 49 * Generates {@link TestMedia} items that can be accessed via test {@link CloudMediaProvider} 50 * instances. 51 */ 52 public class PickerProviderMediaGenerator { 53 private static final Map<String, MediaGenerator> sMediaGeneratorMap = new HashMap<>(); 54 private static final String TAG = "PickerProviderMediaGenerator"; 55 private static final String[] MEDIA_PROJECTION = new String[] { 56 MediaColumns.ID, 57 MediaColumns.MEDIA_STORE_URI, 58 MediaColumns.MIME_TYPE, 59 MediaColumns.STANDARD_MIME_TYPE_EXTENSION, 60 MediaColumns.DATE_TAKEN_MILLIS, 61 MediaColumns.SYNC_GENERATION, 62 MediaColumns.SIZE_BYTES, 63 MediaColumns.DURATION_MILLIS, 64 MediaColumns.IS_FAVORITE, 65 }; 66 private static final String[] ALBUM_MEDIA_PROJECTION = new String[] { 67 MediaColumns.ID, 68 MediaColumns.MEDIA_STORE_URI, 69 MediaColumns.MIME_TYPE, 70 MediaColumns.STANDARD_MIME_TYPE_EXTENSION, 71 MediaColumns.DATE_TAKEN_MILLIS, 72 MediaColumns.SYNC_GENERATION, 73 MediaColumns.SIZE_BYTES, 74 MediaColumns.DURATION_MILLIS, 75 }; 76 77 private static final String[] ALBUM_PROJECTION = new String[] { 78 AlbumColumns.ID, 79 AlbumColumns.DATE_TAKEN_MILLIS, 80 AlbumColumns.DISPLAY_NAME, 81 AlbumColumns.MEDIA_COVER_ID, 82 AlbumColumns.MEDIA_COUNT, 83 AlbumColumns.AUTHORITY 84 }; 85 86 private static final String[] DELETED_MEDIA_PROJECTION = new String[] { MediaColumns.ID }; 87 88 public static class MediaGenerator { 89 private final List<TestMedia> mMedia = new ArrayList<>(); 90 private final List<TestMedia> mDeletedMedia = new ArrayList<>(); 91 private final List<TestAlbum> mAlbums = new ArrayList<>(); 92 private String mCollectionId; 93 private long mLastSyncGeneration; 94 private String mAccountName; 95 private Intent mAccountConfigurationIntent; 96 private int mCursorExtraQueryCount; 97 private Bundle mCursorExtra; 98 private Integer mNextPageToken; 99 getMedia( long generation, String albumId, String[] mimeTypes, long sizeBytes, int pageSize)100 public Cursor getMedia( 101 long generation, String albumId, String[] mimeTypes, long sizeBytes, int pageSize) { 102 return getMedia(generation, albumId, mimeTypes, sizeBytes, null, pageSize); 103 } 104 getMedia( long generation, String albumId, String[] mimeTypes, long sizeBytes, String pageToken, int pageSize)105 public Cursor getMedia( 106 long generation, 107 String albumId, 108 String[] mimeTypes, 109 long sizeBytes, 110 String pageToken, 111 int pageSize) { 112 final Cursor cursor = 113 getCursor( 114 mMedia, 115 generation, 116 albumId, 117 mimeTypes, 118 sizeBytes, 119 /* isDeleted */ false, 120 pageToken); 121 122 if (mCursorExtra != null) { 123 cursor.setExtras(mCursorExtra); 124 } else { 125 cursor.setExtras( 126 buildCursorExtras( 127 mCollectionId, generation > 0, albumId != null, mNextPageToken, 128 pageSize > -1)); 129 mNextPageToken = null; 130 } 131 132 if (--mCursorExtraQueryCount == 0) { 133 clearCursorExtras(); 134 } 135 return cursor; 136 } 137 getAlbums(String[] mimeTypes, long sizeBytes, boolean isLocal)138 public Cursor getAlbums(String[] mimeTypes, long sizeBytes, boolean isLocal) { 139 return getAlbums(mimeTypes, sizeBytes, isLocal, /* pageToken= */ null); 140 } 141 getAlbums( String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken)142 public Cursor getAlbums( 143 String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken) { 144 final Cursor cursor = getCursor(mAlbums, mimeTypes, sizeBytes, isLocal, pageToken); 145 146 if (mCursorExtra != null) { 147 cursor.setExtras(mCursorExtra); 148 } else { 149 cursor.setExtras(buildCursorExtras(mCollectionId, false, false, mNextPageToken, 150 false)); 151 mNextPageToken = null; 152 } 153 154 if (--mCursorExtraQueryCount == 0) { 155 clearCursorExtras(); 156 } 157 return cursor; 158 } 159 getDeletedMedia(long generation)160 public Cursor getDeletedMedia(long generation) { 161 return getDeletedMedia(generation, /* pageToken= */ null); 162 } getDeletedMedia(long generation, String pageToken)163 public Cursor getDeletedMedia(long generation, String pageToken) { 164 final Cursor cursor = getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT, 165 STRING_ARRAY_DEFAULT, /* sizeBytes */ LONG_DEFAULT, 166 /* isDeleted */ true, pageToken); 167 168 if (mCursorExtra != null) { 169 cursor.setExtras(mCursorExtra); 170 } else { 171 cursor.setExtras( 172 buildCursorExtras(mCollectionId, generation > 0, false, mNextPageToken, 173 false)); 174 mNextPageToken = null; 175 } 176 177 if (--mCursorExtraQueryCount == 0) { 178 clearCursorExtras(); 179 } 180 return cursor; 181 } 182 getMediaCollectionInfo()183 public Bundle getMediaCollectionInfo() { 184 Bundle bundle = new Bundle(); 185 bundle.putString(MediaCollectionInfo.MEDIA_COLLECTION_ID, mCollectionId); 186 bundle.putLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, mLastSyncGeneration); 187 bundle.putString(MediaCollectionInfo.ACCOUNT_NAME, mAccountName); 188 bundle.putParcelable(MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT, 189 mAccountConfigurationIntent); 190 191 return bundle; 192 } 193 setAccountInfo(String accountName, Intent configIntent)194 public void setAccountInfo(String accountName, Intent configIntent) { 195 mAccountName = accountName; 196 mAccountConfigurationIntent = configIntent; 197 } 198 clearCursorExtras()199 public void clearCursorExtras() { 200 mCursorExtra = null; 201 } 202 setNextCursorExtras(int queryCount, String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumId, boolean honouredPageSize)203 public void setNextCursorExtras(int queryCount, String mediaCollectionId, 204 boolean honoredSyncGeneration, boolean honoredAlbumId, boolean honouredPageSize) { 205 mCursorExtraQueryCount = queryCount; 206 mCursorExtra = 207 buildCursorExtras( 208 mediaCollectionId, 209 honoredSyncGeneration, 210 honoredAlbumId, 211 mNextPageToken, 212 honouredPageSize); 213 } 214 buildCursorExtras( String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumdId, Integer pageToken, boolean honouredPageSize)215 public Bundle buildCursorExtras( 216 String mediaCollectionId, 217 boolean honoredSyncGeneration, 218 boolean honoredAlbumdId, 219 Integer pageToken, 220 boolean honouredPageSize) { 221 final ArrayList<String> honoredArgs = new ArrayList<>(); 222 if (honoredSyncGeneration) { 223 honoredArgs.add(EXTRA_SYNC_GENERATION); 224 } 225 if (honoredAlbumdId) { 226 honoredArgs.add(EXTRA_ALBUM_ID); 227 } 228 229 if (honouredPageSize) { 230 honoredArgs.add(EXTRA_PAGE_SIZE); 231 } 232 233 final Bundle bundle = new Bundle(); 234 bundle.putString( 235 CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID, mediaCollectionId); 236 bundle.putStringArrayList(ContentResolver.EXTRA_HONORED_ARGS, honoredArgs); 237 if (pageToken != null) { 238 bundle.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, pageToken.toString()); 239 } 240 241 return bundle; 242 } 243 addMedia(String localId, String cloudId)244 public void addMedia(String localId, String cloudId) { 245 mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId)); 246 mMedia.add(0, createTestMedia(localId, cloudId)); 247 } 248 addAlbumMedia(String localId, String cloudId, String albumId)249 public void addAlbumMedia(String localId, String cloudId, String albumId) { 250 mMedia.add(0, createTestAlbumMedia(localId, cloudId, albumId)); 251 } 252 addMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)253 public void addMedia(String localId, String cloudId, String albumId, String mimeType, 254 int standardMimeTypeExtension, long sizeBytes, boolean isFavorite) { 255 mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId)); 256 mMedia.add(0, 257 createTestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension, 258 sizeBytes, isFavorite)); 259 } 260 deleteMedia(String localId, String cloudId)261 public void deleteMedia(String localId, String cloudId) { 262 if (mMedia.remove(createPlaceholderMedia(localId, cloudId))) { 263 mDeletedMedia.add(createTestMedia(localId, cloudId)); 264 } 265 } 266 createAlbum(String id)267 public void createAlbum(String id) { 268 mAlbums.add(createTestAlbum(id)); 269 } 270 resetAll()271 public void resetAll() { 272 mMedia.clear(); 273 mDeletedMedia.clear(); 274 mAlbums.clear(); 275 clearCursorExtras(); 276 mNextPageToken = null; 277 } 278 setMediaCollectionId(String id)279 public void setMediaCollectionId(String id) { 280 mCollectionId = id; 281 } 282 getCount()283 public long getCount() { 284 return mMedia.size(); 285 } 286 createTestAlbum(String id)287 private TestAlbum createTestAlbum(String id) { 288 return new TestAlbum(id, mMedia); 289 } 290 createTestMedia(String localId, String cloudId)291 private TestMedia createTestMedia(String localId, String cloudId) { 292 // Increase generation 293 return new TestMedia(localId, cloudId, ++mLastSyncGeneration); 294 } 295 createTestAlbumMedia(String localId, String cloudId, String albumId)296 private TestMedia createTestAlbumMedia(String localId, String cloudId, String albumId) { 297 // Increase generation 298 return new TestMedia(localId, cloudId, albumId); 299 } 300 createTestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)301 private TestMedia createTestMedia(String localId, String cloudId, String albumId, 302 String mimeType, int standardMimeTypeExtension, long sizeBytes, 303 boolean isFavorite) { 304 // Increase generation 305 return new TestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension, 306 sizeBytes, /* durationMs */ 0, ++mLastSyncGeneration, isFavorite); 307 } 308 createPlaceholderMedia(String localId, String cloudId)309 private static TestMedia createPlaceholderMedia(String localId, String cloudId) { 310 // Don't increase generation. Used to create a throw-away element used for removal from 311 // |mMedia| or |mDeletedMedia| 312 return new TestMedia(localId, cloudId, 0); 313 } 314 getCursor( List<TestMedia> mediaList, long generation, String albumId, String[] mimeTypes, long sizeBytes, boolean isDeleted, String pageToken)315 private Cursor getCursor( 316 List<TestMedia> mediaList, 317 long generation, 318 String albumId, 319 String[] mimeTypes, 320 long sizeBytes, 321 boolean isDeleted, 322 String pageToken) { 323 final MatrixCursor matrix; 324 final int pageSize = 5; 325 326 if (isDeleted) { 327 matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION); 328 } else if (!TextUtils.isEmpty(albumId)) { 329 matrix = new MatrixCursor(ALBUM_MEDIA_PROJECTION); 330 } else { 331 matrix = new MatrixCursor(MEDIA_PROJECTION); 332 } 333 334 int page = 0; 335 if (pageToken != null) { 336 page = Integer.parseInt(pageToken); 337 } 338 339 // Calculate the starting position: pageSize * pageNumber 340 int startPosition = (pageSize * page); 341 // Calculate the end of the page 342 int endPosition = startPosition + pageSize; 343 344 for (int i = startPosition; i < endPosition; i++) { 345 346 try { 347 TestMedia media = mediaList.get(i); 348 if (!TextUtils.isEmpty(albumId) 349 && matchesFilter(media, albumId, mimeTypes, sizeBytes)) { 350 matrix.addRow(media.toAlbumMediaArray()); 351 } else if (media.generation > generation 352 && matchesFilter(media, albumId, mimeTypes, sizeBytes)) { 353 matrix.addRow(media.toArray(isDeleted)); 354 } 355 356 } catch (IndexOutOfBoundsException e) { 357 // We're at the end of the list, before the end of the page so break the loop. 358 break; 359 } 360 } 361 362 // Set next page token if there is another page. 363 if (mediaList.size() > endPosition) { 364 mNextPageToken = Integer.valueOf(++page); 365 } else { 366 mNextPageToken = null; 367 } 368 369 return matrix; 370 } 371 getCursor(List<TestAlbum> albumList, String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken)372 private static Cursor getCursor(List<TestAlbum> albumList, String[] mimeTypes, 373 long sizeBytes, boolean isLocal, String pageToken) { 374 final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION); 375 final int pageSize = 5; 376 377 int page = 0; 378 if (pageToken != null) { 379 page = Integer.parseInt(pageToken); 380 } 381 382 // Calculate the starting position: pageSize * pageNumber 383 int startPosition = (pageSize * page); 384 // Calculate the end of the page 385 int endPosition = startPosition + pageSize; 386 387 388 for (int i = startPosition; i < endPosition; i++) { 389 390 try { 391 TestAlbum album = albumList.get(i); 392 final String[] res = album.toArray(mimeTypes, sizeBytes, isLocal); 393 if (res != null) { 394 matrix.addRow(res); 395 } 396 } catch (IndexOutOfBoundsException e) { 397 // We're at the end of the list, before the end of the page so break the loop. 398 break; 399 } 400 } 401 return matrix; 402 } 403 } 404 405 private static class TestMedia { 406 public final String localId; 407 public final String cloudId; 408 public final String albumId; 409 public final String mimeType; 410 public final int standardMimeTypeExtension; 411 public final long sizeBytes; 412 public final long dateTakenMs; 413 public final long durationMs; 414 public final long generation; 415 public final boolean isFavorite; 416 TestMedia(String localId, String cloudId, long generation)417 public TestMedia(String localId, String cloudId, long generation) { 418 this(localId, cloudId, /* albumId */ null, "image/jpeg", 419 /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, 420 /* sizeBytes */ 4096, /* durationMs */ 0, generation, 421 /* isFavorite */ false); 422 } 423 424 TestMedia(String localId, String cloudId, String albumId)425 public TestMedia(String localId, String cloudId, String albumId) { 426 this(localId, cloudId, /* albumId */ albumId, "image/jpeg", 427 /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, 428 /* sizeBytes */ 4096, /* durationMs */ 0, 0, 429 /* isFavorite */ false); 430 } 431 TestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation, boolean isFavorite)432 public TestMedia(String localId, String cloudId, String albumId, String mimeType, 433 int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation, 434 boolean isFavorite) { 435 this.localId = localId; 436 this.cloudId = cloudId; 437 this.albumId = albumId; 438 this.mimeType = mimeType; 439 this.standardMimeTypeExtension = standardMimeTypeExtension; 440 this.sizeBytes = sizeBytes; 441 this.dateTakenMs = System.currentTimeMillis(); 442 this.durationMs = durationMs; 443 this.generation = generation; 444 this.isFavorite = isFavorite; 445 SystemClock.sleep(1); 446 } 447 toArray(boolean isDeleted)448 public String[] toArray(boolean isDeleted) { 449 if (isDeleted) { 450 return new String[] {getId()}; 451 } 452 453 return new String[] { 454 getId(), 455 localId == null ? null : "content://media/external/files/" + localId, 456 mimeType, 457 String.valueOf(standardMimeTypeExtension), 458 String.valueOf(dateTakenMs), 459 String.valueOf(generation), 460 String.valueOf(sizeBytes), 461 String.valueOf(durationMs), 462 String.valueOf(isFavorite ? 1 : 0) 463 }; 464 } 465 toAlbumMediaArray()466 public String[] toAlbumMediaArray() { 467 return new String[] { 468 getId(), 469 localId == null ? null : "content://media/external/files/" + localId, 470 mimeType, 471 String.valueOf(standardMimeTypeExtension), 472 String.valueOf(dateTakenMs), 473 String.valueOf(generation), 474 String.valueOf(sizeBytes), 475 String.valueOf(durationMs) 476 }; 477 } 478 479 @Override equals(Object o)480 public boolean equals(Object o) { 481 if (o == null || !(o instanceof TestMedia)) { 482 return false; 483 } 484 TestMedia other = (TestMedia) o; 485 return Objects.equals(localId, other.localId) && Objects.equals(cloudId, other.cloudId); 486 } 487 488 @Override hashCode()489 public int hashCode() { 490 return Objects.hash(localId, cloudId); 491 } 492 getId()493 private String getId() { 494 return cloudId == null ? localId : cloudId; 495 } 496 } 497 498 private static class TestAlbum { 499 public final String id; 500 private final List<TestMedia> media; 501 TestAlbum(String id, List<TestMedia> media)502 public TestAlbum(String id, List<TestMedia> media) { 503 this.id = id; 504 this.media = media; 505 } 506 toArray(String[] mimeTypes, long sizeBytes, boolean isLocal)507 public String[] toArray(String[] mimeTypes, long sizeBytes, boolean isLocal) { 508 long mediaCount = 0; 509 String mediaCoverId = null; 510 long dateTakenMs = 0; 511 512 for (TestMedia m : media) { 513 if (matchesFilter(m, id, mimeTypes, sizeBytes)) { 514 if (mediaCount++ == 0) { 515 mediaCoverId = m.getId(); 516 dateTakenMs = m.dateTakenMs; 517 } 518 } 519 } 520 521 if (mediaCount == 0) { 522 return null; 523 } 524 525 return new String[] { 526 id, 527 String.valueOf(dateTakenMs), 528 /* displayName */ id, 529 mediaCoverId, 530 String.valueOf(mediaCount), 531 isLocal ? LocalProvider.AUTHORITY : null 532 }; 533 } 534 535 @Override equals(Object o)536 public boolean equals(Object o) { 537 if (o == null || !(o instanceof TestAlbum)) { 538 return false; 539 } 540 541 TestAlbum other = (TestAlbum) o; 542 return Objects.equals(id, other.id); 543 } 544 545 @Override hashCode()546 public int hashCode() { 547 return Objects.hash(id); 548 } 549 } 550 matchesFilter(TestMedia media, String albumId, String[] mimeTypes, long sizeBytes)551 private static boolean matchesFilter(TestMedia media, String albumId, String[] mimeTypes, 552 long sizeBytes) { 553 if (!Objects.equals(albumId, STRING_DEFAULT) && !Objects.equals(albumId, media.albumId)) { 554 return false; 555 } 556 557 if (mimeTypes != null) { 558 boolean matchesMimeType = false; 559 for (String m : mimeTypes) { 560 if (m != null && media.mimeType.startsWith(m)) { 561 matchesMimeType = true; 562 break; 563 } 564 } 565 566 if (!matchesMimeType) { 567 return false; 568 } 569 } 570 571 if (sizeBytes != LONG_DEFAULT && media.sizeBytes > sizeBytes) { 572 return false; 573 } 574 575 return true; 576 } 577 getMediaGenerator(String authority)578 public static MediaGenerator getMediaGenerator(String authority) { 579 MediaGenerator generator = sMediaGeneratorMap.get(authority); 580 if (generator == null) { 581 generator = new MediaGenerator(); 582 sMediaGeneratorMap.put(authority, generator); 583 } 584 return generator; 585 } 586 } 587