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_MEDIA_COLLECTION_ID; 22 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION; 23 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo; 24 import static android.provider.CloudMediaProviderContract.MediaColumns; 25 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT; 26 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT; 27 28 import android.content.ContentResolver; 29 import android.content.Intent; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.os.Bundle; 33 import android.os.SystemClock; 34 import android.provider.CloudMediaProvider; 35 import android.provider.CloudMediaProviderContract; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import com.android.providers.media.photopicker.LocalProvider; 40 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 47 /** 48 * Generates {@link TestMedia} items that can be accessed via test {@link CloudMediaProvider} 49 * instances. 50 */ 51 public class PickerProviderMediaGenerator { 52 private static final Map<String, MediaGenerator> sMediaGeneratorMap = new HashMap<>(); 53 private static final String TAG = "PickerProviderMediaGenerator"; 54 private static final String[] MEDIA_PROJECTION = new String[] { 55 MediaColumns.ID, 56 MediaColumns.MEDIA_STORE_URI, 57 MediaColumns.MIME_TYPE, 58 MediaColumns.STANDARD_MIME_TYPE_EXTENSION, 59 MediaColumns.DATE_TAKEN_MILLIS, 60 MediaColumns.SYNC_GENERATION, 61 MediaColumns.SIZE_BYTES, 62 MediaColumns.DURATION_MILLIS, 63 MediaColumns.IS_FAVORITE, 64 }; 65 private static final String[] ALBUM_MEDIA_PROJECTION = new String[] { 66 MediaColumns.ID, 67 MediaColumns.MEDIA_STORE_URI, 68 MediaColumns.MIME_TYPE, 69 MediaColumns.STANDARD_MIME_TYPE_EXTENSION, 70 MediaColumns.DATE_TAKEN_MILLIS, 71 MediaColumns.SYNC_GENERATION, 72 MediaColumns.SIZE_BYTES, 73 MediaColumns.DURATION_MILLIS, 74 }; 75 76 private static final String[] ALBUM_PROJECTION = new String[] { 77 AlbumColumns.ID, 78 AlbumColumns.DISPLAY_NAME, 79 AlbumColumns.DATE_TAKEN_MILLIS, 80 AlbumColumns.MEDIA_COVER_ID, 81 AlbumColumns.MEDIA_COUNT, 82 AlbumColumns.AUTHORITY 83 }; 84 85 private static final String[] DELETED_MEDIA_PROJECTION = new String[] { MediaColumns.ID }; 86 87 public static class MediaGenerator { 88 private final List<TestMedia> mMedia = new ArrayList<>(); 89 private final List<TestMedia> mDeletedMedia = new ArrayList<>(); 90 private final List<TestAlbum> mAlbums = new ArrayList<>(); 91 private String mCollectionId; 92 private long mLastSyncGeneration; 93 private String mAccountName; 94 private Intent mAccountConfigurationIntent; 95 private int mCursorExtraQueryCount; 96 private Bundle mCursorExtra; 97 98 // TODO(b/214592293): Add pagination support for testing purposes. getMedia(long generation, String albumId, String mimeType, long sizeBytes)99 public Cursor getMedia(long generation, String albumId, String mimeType, long sizeBytes) { 100 final Cursor cursor = getCursor(mMedia, generation, albumId, mimeType, sizeBytes, 101 /* isDeleted */ false); 102 103 if (mCursorExtra != null) { 104 cursor.setExtras(mCursorExtra); 105 } else { 106 cursor.setExtras(buildCursorExtras(mCollectionId, generation > 0, albumId != null)); 107 } 108 109 if (--mCursorExtraQueryCount == 0) { 110 clearCursorExtras(); 111 } 112 return cursor; 113 } 114 getAlbums(String mimeType, long sizeBytes, boolean isLocal)115 public Cursor getAlbums(String mimeType, long sizeBytes, boolean isLocal) { 116 final Cursor cursor = getCursor(mAlbums, mimeType, sizeBytes, isLocal); 117 118 if (mCursorExtra != null) { 119 cursor.setExtras(mCursorExtra); 120 } else { 121 cursor.setExtras(buildCursorExtras(mCollectionId, false, false)); 122 } 123 124 if (--mCursorExtraQueryCount == 0) { 125 clearCursorExtras(); 126 } 127 return cursor; 128 } 129 130 // TODO(b/214592293): Add pagination support for testing purposes. getDeletedMedia(long generation)131 public Cursor getDeletedMedia(long generation) { 132 final Cursor cursor = getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT, 133 /* mimeType */ STRING_DEFAULT, /* sizeBytes */ LONG_DEFAULT, 134 /* isDeleted */ true); 135 136 if (mCursorExtra != null) { 137 cursor.setExtras(mCursorExtra); 138 } else { 139 cursor.setExtras(buildCursorExtras(mCollectionId, generation > 0, false)); 140 } 141 142 if (--mCursorExtraQueryCount == 0) { 143 clearCursorExtras(); 144 } 145 return cursor; 146 } 147 getMediaCollectionInfo()148 public Bundle getMediaCollectionInfo() { 149 Bundle bundle = new Bundle(); 150 bundle.putString(MediaCollectionInfo.MEDIA_COLLECTION_ID, mCollectionId); 151 bundle.putLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, mLastSyncGeneration); 152 bundle.putString(MediaCollectionInfo.ACCOUNT_NAME, mAccountName); 153 bundle.putParcelable(MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT, 154 mAccountConfigurationIntent); 155 156 return bundle; 157 } 158 setAccountInfo(String accountName, Intent configIntent)159 public void setAccountInfo(String accountName, Intent configIntent) { 160 mAccountName = accountName; 161 mAccountConfigurationIntent = configIntent; 162 } 163 clearCursorExtras()164 public void clearCursorExtras() { 165 mCursorExtra = null; 166 } 167 setNextCursorExtras(int queryCount, String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumId)168 public void setNextCursorExtras(int queryCount, String mediaCollectionId, 169 boolean honoredSyncGeneration, boolean honoredAlbumId) { 170 mCursorExtraQueryCount = queryCount; 171 mCursorExtra = buildCursorExtras(mediaCollectionId, honoredSyncGeneration, 172 honoredAlbumId); 173 } 174 buildCursorExtras(String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumdId)175 public Bundle buildCursorExtras(String mediaCollectionId, boolean honoredSyncGeneration, 176 boolean honoredAlbumdId) { 177 final ArrayList<String> honoredArgs = new ArrayList<>(); 178 if (honoredSyncGeneration) { 179 honoredArgs.add(EXTRA_SYNC_GENERATION); 180 } 181 if (honoredAlbumdId) { 182 honoredArgs.add(EXTRA_ALBUM_ID); 183 } 184 185 final Bundle bundle = new Bundle(); 186 bundle.putString(CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID, 187 mediaCollectionId); 188 bundle.putStringArrayList(ContentResolver.EXTRA_HONORED_ARGS, honoredArgs); 189 190 return bundle; 191 } 192 addMedia(String localId, String cloudId)193 public void addMedia(String localId, String cloudId) { 194 mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId)); 195 mMedia.add(0, createTestMedia(localId, cloudId)); 196 } 197 addAlbumMedia(String localId, String cloudId, String albumId)198 public void addAlbumMedia(String localId, String cloudId, String albumId) { 199 mMedia.add(0, createTestAlbumMedia(localId, cloudId, albumId)); 200 } 201 addMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)202 public void addMedia(String localId, String cloudId, String albumId, String mimeType, 203 int standardMimeTypeExtension, long sizeBytes, boolean isFavorite) { 204 mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId)); 205 mMedia.add(0, 206 createTestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension, 207 sizeBytes, isFavorite)); 208 } 209 deleteMedia(String localId, String cloudId)210 public void deleteMedia(String localId, String cloudId) { 211 if (mMedia.remove(createPlaceholderMedia(localId, cloudId))) { 212 mDeletedMedia.add(createTestMedia(localId, cloudId)); 213 } 214 } 215 createAlbum(String id)216 public void createAlbum(String id) { 217 mAlbums.add(createTestAlbum(id)); 218 } 219 resetAll()220 public void resetAll() { 221 mMedia.clear(); 222 mDeletedMedia.clear(); 223 mAlbums.clear(); 224 clearCursorExtras(); 225 } 226 setMediaCollectionId(String id)227 public void setMediaCollectionId(String id) { 228 mCollectionId = id; 229 } 230 getCount()231 public long getCount() { 232 return mMedia.size(); 233 } 234 createTestAlbum(String id)235 private TestAlbum createTestAlbum(String id) { 236 return new TestAlbum(id, mMedia); 237 } 238 createTestMedia(String localId, String cloudId)239 private TestMedia createTestMedia(String localId, String cloudId) { 240 // Increase generation 241 return new TestMedia(localId, cloudId, ++mLastSyncGeneration); 242 } createTestAlbumMedia(String localId, String cloudId, String albumId)243 private TestMedia createTestAlbumMedia(String localId, String cloudId, String albumId) { 244 // Increase generation 245 return new TestMedia(localId, cloudId, albumId); 246 } 247 createTestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)248 private TestMedia createTestMedia(String localId, String cloudId, String albumId, 249 String mimeType, int standardMimeTypeExtension, long sizeBytes, 250 boolean isFavorite) { 251 // Increase generation 252 return new TestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension, 253 sizeBytes, /* durationMs */ 0, ++mLastSyncGeneration, isFavorite); 254 } 255 createPlaceholderMedia(String localId, String cloudId)256 private static TestMedia createPlaceholderMedia(String localId, String cloudId) { 257 // Don't increase generation. Used to create a throw-away element used for removal from 258 // |mMedia| or |mDeletedMedia| 259 return new TestMedia(localId, cloudId, 0); 260 } 261 getCursor(List<TestMedia> mediaList, long generation, String albumId, String mimeType, long sizeBytes, boolean isDeleted)262 private static Cursor getCursor(List<TestMedia> mediaList, long generation, 263 String albumId, String mimeType, long sizeBytes, boolean isDeleted) { 264 final MatrixCursor matrix; 265 if (isDeleted) { 266 matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION); 267 } else if(!TextUtils.isEmpty(albumId)) { 268 matrix = new MatrixCursor(ALBUM_MEDIA_PROJECTION); 269 } else { 270 matrix = new MatrixCursor(MEDIA_PROJECTION); 271 } 272 273 for (TestMedia media : mediaList) { 274 if (!TextUtils.isEmpty(albumId) && matchesFilter(media, 275 albumId, mimeType, sizeBytes)) { 276 matrix.addRow(media.toAlbumMediaArray()); 277 } else if (media.generation > generation 278 && matchesFilter(media, albumId, mimeType, sizeBytes)) { 279 matrix.addRow(media.toArray(isDeleted)); 280 } 281 } 282 return matrix; 283 } 284 getCursor(List<TestAlbum> albumList, String mimeType, long sizeBytes, boolean isLocal)285 private static Cursor getCursor(List<TestAlbum> albumList, String mimeType, long sizeBytes, 286 boolean isLocal) { 287 final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION); 288 289 for (TestAlbum album : albumList) { 290 final String[] res = album.toArray(mimeType, sizeBytes, isLocal); 291 if (res != null) { 292 matrix.addRow(res); 293 } 294 } 295 return matrix; 296 } 297 } 298 299 private static class TestMedia { 300 public final String localId; 301 public final String cloudId; 302 public final String albumId; 303 public final String mimeType; 304 public final int standardMimeTypeExtension; 305 public final long sizeBytes; 306 public final long dateTakenMs; 307 public final long durationMs; 308 public final long generation; 309 public final boolean isFavorite; 310 TestMedia(String localId, String cloudId, long generation)311 public TestMedia(String localId, String cloudId, long generation) { 312 this(localId, cloudId, /* albumId */ null, "image/jpeg", 313 /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, 314 /* sizeBytes */ 4096, /* durationMs */ 0, generation, 315 /* isFavorite */ false); 316 } 317 318 TestMedia(String localId, String cloudId, String albumId)319 public TestMedia(String localId, String cloudId, String albumId) { 320 this(localId, cloudId, /* albumId */ albumId, "image/jpeg", 321 /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, 322 /* sizeBytes */ 4096, /* durationMs */ 0, 0, 323 /* isFavorite */ false); 324 } 325 TestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation, boolean isFavorite)326 public TestMedia(String localId, String cloudId, String albumId, String mimeType, 327 int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation, 328 boolean isFavorite) { 329 this.localId = localId; 330 this.cloudId = cloudId; 331 this.albumId = albumId; 332 this.mimeType = mimeType; 333 this.standardMimeTypeExtension = standardMimeTypeExtension; 334 this.sizeBytes = sizeBytes; 335 this.dateTakenMs = System.currentTimeMillis(); 336 this.durationMs = durationMs; 337 this.generation = generation; 338 this.isFavorite = isFavorite; 339 SystemClock.sleep(1); 340 } 341 toArray(boolean isDeleted)342 public String[] toArray(boolean isDeleted) { 343 if (isDeleted) { 344 return new String[] {getId()}; 345 } 346 347 return new String[] { 348 getId(), 349 localId == null ? null : "content://media/external/files/" + localId, 350 mimeType, 351 String.valueOf(standardMimeTypeExtension), 352 String.valueOf(dateTakenMs), 353 String.valueOf(generation), 354 String.valueOf(sizeBytes), 355 String.valueOf(durationMs), 356 String.valueOf(isFavorite ? 1 : 0) 357 }; 358 } 359 toAlbumMediaArray()360 public String[] toAlbumMediaArray() { 361 return new String[] { 362 getId(), 363 localId == null ? null : "content://media/external/files/" + localId, 364 mimeType, 365 String.valueOf(standardMimeTypeExtension), 366 String.valueOf(dateTakenMs), 367 String.valueOf(generation), 368 String.valueOf(sizeBytes), 369 String.valueOf(durationMs) 370 }; 371 } 372 373 @Override equals(Object o)374 public boolean equals(Object o) { 375 if (o == null || !(o instanceof TestMedia)) { 376 return false; 377 } 378 TestMedia other = (TestMedia) o; 379 return Objects.equals(localId, other.localId) && Objects.equals(cloudId, other.cloudId); 380 } 381 382 @Override hashCode()383 public int hashCode() { 384 return Objects.hash(localId, cloudId); 385 } 386 getId()387 private String getId() { 388 return cloudId == null ? localId : cloudId; 389 } 390 } 391 392 private static class TestAlbum { 393 public final String id; 394 private final List<TestMedia> media; 395 TestAlbum(String id, List<TestMedia> media)396 public TestAlbum(String id, List<TestMedia> media) { 397 this.id = id; 398 this.media = media; 399 } 400 toArray(String mimeType, long sizeBytes, boolean isLocal)401 public String[] toArray(String mimeType, long sizeBytes, boolean isLocal) { 402 long mediaCount = 0; 403 String mediaCoverId = null; 404 long dateTakenMs = 0; 405 406 for (TestMedia m : media) { 407 if (matchesFilter(m, id, mimeType, sizeBytes)) { 408 if (mediaCount++ == 0) { 409 mediaCoverId = m.getId(); 410 dateTakenMs = m.dateTakenMs; 411 } 412 } 413 } 414 415 if (mediaCount == 0) { 416 return null; 417 } 418 419 return new String[] { 420 id, 421 mediaCoverId, 422 /* displayName */ id, 423 String.valueOf(dateTakenMs), 424 String.valueOf(mediaCount), 425 isLocal ? LocalProvider.AUTHORITY : null 426 }; 427 } 428 429 @Override equals(Object o)430 public boolean equals(Object o) { 431 if (o == null || !(o instanceof TestAlbum)) { 432 return false; 433 } 434 435 TestAlbum other = (TestAlbum) o; 436 return Objects.equals(id, other.id); 437 } 438 439 @Override hashCode()440 public int hashCode() { 441 return Objects.hash(id); 442 } 443 } 444 matchesFilter(TestMedia media, String albumId, String mimeType, long sizeBytes)445 private static boolean matchesFilter(TestMedia media, String albumId, String mimeType, 446 long sizeBytes) { 447 if (!Objects.equals(albumId, STRING_DEFAULT) && !Objects.equals(albumId, media.albumId)) { 448 return false; 449 } 450 if (!Objects.equals(mimeType, STRING_DEFAULT) && !media.mimeType.startsWith(mimeType)) { 451 return false; 452 } 453 if (sizeBytes != LONG_DEFAULT && media.sizeBytes > sizeBytes) { 454 return false; 455 } 456 457 return true; 458 } 459 getMediaGenerator(String authority)460 public static MediaGenerator getMediaGenerator(String authority) { 461 MediaGenerator generator = sMediaGeneratorMap.get(authority); 462 if (generator == null) { 463 generator = new MediaGenerator(); 464 sMediaGeneratorMap.put(authority, generator); 465 } 466 return generator; 467 } 468 } 469