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