• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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