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