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