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_PAGE_SIZE;
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 static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
27 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_ARRAY_DEFAULT;
28 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
29 
30 import android.content.ContentResolver;
31 import android.content.Intent;
32 import android.database.Cursor;
33 import android.database.MatrixCursor;
34 import android.os.Bundle;
35 import android.os.SystemClock;
36 import android.provider.CloudMediaProvider;
37 import android.provider.CloudMediaProviderContract;
38 import android.text.TextUtils;
39 
40 import com.android.providers.media.photopicker.LocalProvider;
41 
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 
48 /**
49  * Generates {@link TestMedia} items that can be accessed via test {@link CloudMediaProvider}
50  * instances.
51  */
52 public class PickerProviderMediaGenerator {
53     private static final Map<String, MediaGenerator> sMediaGeneratorMap = new HashMap<>();
54     private static final String TAG = "PickerProviderMediaGenerator";
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     private static final String[] ALBUM_MEDIA_PROJECTION = new String[] {
67             MediaColumns.ID,
68             MediaColumns.MEDIA_STORE_URI,
69             MediaColumns.MIME_TYPE,
70             MediaColumns.STANDARD_MIME_TYPE_EXTENSION,
71             MediaColumns.DATE_TAKEN_MILLIS,
72             MediaColumns.SYNC_GENERATION,
73             MediaColumns.SIZE_BYTES,
74             MediaColumns.DURATION_MILLIS,
75     };
76 
77     private static final String[] ALBUM_PROJECTION = new String[] {
78         AlbumColumns.ID,
79         AlbumColumns.DATE_TAKEN_MILLIS,
80         AlbumColumns.DISPLAY_NAME,
81         AlbumColumns.MEDIA_COVER_ID,
82         AlbumColumns.MEDIA_COUNT,
83         AlbumColumns.AUTHORITY
84     };
85 
86     private static final String[] DELETED_MEDIA_PROJECTION = new String[] { MediaColumns.ID };
87 
88     public static class MediaGenerator {
89         private final List<TestMedia> mMedia = new ArrayList<>();
90         private final List<TestMedia> mDeletedMedia = new ArrayList<>();
91         private final List<TestAlbum> mAlbums = new ArrayList<>();
92         private String mCollectionId;
93         private long mLastSyncGeneration;
94         private String mAccountName;
95         private Intent mAccountConfigurationIntent;
96         private int mCursorExtraQueryCount;
97         private Bundle mCursorExtra;
98         private Integer mNextPageToken;
99 
getMedia( long generation, String albumId, String[] mimeTypes, long sizeBytes, int pageSize)100         public Cursor getMedia(
101                 long generation, String albumId, String[] mimeTypes, long sizeBytes, int pageSize) {
102             return getMedia(generation, albumId, mimeTypes, sizeBytes, null, pageSize);
103         }
104 
getMedia( long generation, String albumId, String[] mimeTypes, long sizeBytes, String pageToken, int pageSize)105         public Cursor getMedia(
106                 long generation,
107                 String albumId,
108                 String[] mimeTypes,
109                 long sizeBytes,
110                 String pageToken,
111                 int pageSize) {
112             final Cursor cursor =
113                     getCursor(
114                             mMedia,
115                             generation,
116                             albumId,
117                             mimeTypes,
118                             sizeBytes,
119                             /* isDeleted */ false,
120                             pageToken);
121 
122             if (mCursorExtra != null) {
123                 cursor.setExtras(mCursorExtra);
124             } else {
125                 cursor.setExtras(
126                         buildCursorExtras(
127                                 mCollectionId, generation > 0, albumId != null, mNextPageToken,
128                                 pageSize > -1));
129                 mNextPageToken = null;
130             }
131 
132             if (--mCursorExtraQueryCount == 0) {
133                 clearCursorExtras();
134             }
135             return cursor;
136         }
137 
getAlbums(String[] mimeTypes, long sizeBytes, boolean isLocal)138         public Cursor getAlbums(String[] mimeTypes, long sizeBytes, boolean isLocal) {
139             return getAlbums(mimeTypes, sizeBytes, isLocal, /* pageToken= */ null);
140         }
141 
getAlbums( String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken)142         public Cursor getAlbums(
143                 String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken) {
144             final Cursor cursor = getCursor(mAlbums, mimeTypes, sizeBytes, isLocal, pageToken);
145 
146             if (mCursorExtra != null) {
147                 cursor.setExtras(mCursorExtra);
148             } else {
149                 cursor.setExtras(buildCursorExtras(mCollectionId, false, false, mNextPageToken,
150                         false));
151                 mNextPageToken = null;
152             }
153 
154             if (--mCursorExtraQueryCount == 0) {
155                 clearCursorExtras();
156             }
157             return cursor;
158         }
159 
getDeletedMedia(long generation)160         public Cursor getDeletedMedia(long generation) {
161             return getDeletedMedia(generation, /* pageToken= */ null);
162         }
getDeletedMedia(long generation, String pageToken)163         public Cursor getDeletedMedia(long generation, String pageToken) {
164             final Cursor cursor = getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT,
165                     STRING_ARRAY_DEFAULT, /* sizeBytes */ LONG_DEFAULT,
166                     /* isDeleted */ true, pageToken);
167 
168             if (mCursorExtra != null) {
169                 cursor.setExtras(mCursorExtra);
170             } else {
171                 cursor.setExtras(
172                         buildCursorExtras(mCollectionId, generation > 0, false, mNextPageToken,
173                                 false));
174                 mNextPageToken = null;
175             }
176 
177             if (--mCursorExtraQueryCount == 0) {
178                 clearCursorExtras();
179             }
180             return cursor;
181         }
182 
getMediaCollectionInfo()183         public Bundle getMediaCollectionInfo() {
184             Bundle bundle = new Bundle();
185             bundle.putString(MediaCollectionInfo.MEDIA_COLLECTION_ID, mCollectionId);
186             bundle.putLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, mLastSyncGeneration);
187             bundle.putString(MediaCollectionInfo.ACCOUNT_NAME, mAccountName);
188             bundle.putParcelable(MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT,
189                     mAccountConfigurationIntent);
190 
191             return bundle;
192         }
193 
setAccountInfo(String accountName, Intent configIntent)194         public void setAccountInfo(String accountName, Intent configIntent) {
195             mAccountName = accountName;
196             mAccountConfigurationIntent = configIntent;
197         }
198 
clearCursorExtras()199         public void clearCursorExtras() {
200             mCursorExtra = null;
201         }
202 
setNextCursorExtras(int queryCount, String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumId, boolean honouredPageSize)203         public void setNextCursorExtras(int queryCount, String mediaCollectionId,
204                 boolean honoredSyncGeneration, boolean honoredAlbumId, boolean honouredPageSize) {
205             mCursorExtraQueryCount = queryCount;
206             mCursorExtra =
207                     buildCursorExtras(
208                             mediaCollectionId,
209                             honoredSyncGeneration,
210                             honoredAlbumId,
211                             mNextPageToken,
212                             honouredPageSize);
213         }
214 
buildCursorExtras( String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumdId, Integer pageToken, boolean honouredPageSize)215         public Bundle buildCursorExtras(
216                 String mediaCollectionId,
217                 boolean honoredSyncGeneration,
218                 boolean honoredAlbumdId,
219                 Integer pageToken,
220                 boolean honouredPageSize) {
221             final ArrayList<String> honoredArgs = new ArrayList<>();
222             if (honoredSyncGeneration) {
223                 honoredArgs.add(EXTRA_SYNC_GENERATION);
224             }
225             if (honoredAlbumdId) {
226                 honoredArgs.add(EXTRA_ALBUM_ID);
227             }
228 
229             if (honouredPageSize) {
230                 honoredArgs.add(EXTRA_PAGE_SIZE);
231             }
232 
233             final Bundle bundle = new Bundle();
234             bundle.putString(
235                     CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID, mediaCollectionId);
236             bundle.putStringArrayList(ContentResolver.EXTRA_HONORED_ARGS, honoredArgs);
237             if (pageToken != null) {
238                 bundle.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, pageToken.toString());
239             }
240 
241             return bundle;
242         }
243 
addMedia(String localId, String cloudId)244         public void addMedia(String localId, String cloudId) {
245             mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
246             mMedia.add(0, createTestMedia(localId, cloudId));
247         }
248 
addAlbumMedia(String localId, String cloudId, String albumId)249         public void addAlbumMedia(String localId, String cloudId, String albumId) {
250             mMedia.add(0, createTestAlbumMedia(localId, cloudId, albumId));
251         }
252 
addMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)253         public void addMedia(String localId, String cloudId, String albumId, String mimeType,
254                 int standardMimeTypeExtension, long sizeBytes, boolean isFavorite) {
255             mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
256             mMedia.add(0,
257                     createTestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension,
258                             sizeBytes, isFavorite));
259         }
260 
deleteMedia(String localId, String cloudId)261         public void deleteMedia(String localId, String cloudId) {
262             if (mMedia.remove(createPlaceholderMedia(localId, cloudId))) {
263                 mDeletedMedia.add(createTestMedia(localId, cloudId));
264             }
265         }
266 
createAlbum(String id)267         public void createAlbum(String id) {
268             mAlbums.add(createTestAlbum(id));
269         }
270 
resetAll()271         public void resetAll() {
272             mMedia.clear();
273             mDeletedMedia.clear();
274             mAlbums.clear();
275             clearCursorExtras();
276             mNextPageToken = null;
277         }
278 
setMediaCollectionId(String id)279         public void setMediaCollectionId(String id) {
280             mCollectionId = id;
281         }
282 
getCount()283         public long getCount() {
284             return mMedia.size();
285         }
286 
createTestAlbum(String id)287         private TestAlbum createTestAlbum(String id) {
288             return new TestAlbum(id, mMedia);
289         }
290 
createTestMedia(String localId, String cloudId)291         private TestMedia createTestMedia(String localId, String cloudId) {
292             // Increase generation
293             return new TestMedia(localId, cloudId, ++mLastSyncGeneration);
294         }
295 
createTestAlbumMedia(String localId, String cloudId, String albumId)296         private TestMedia createTestAlbumMedia(String localId, String cloudId, String albumId) {
297             // Increase generation
298             return new TestMedia(localId, cloudId, albumId);
299         }
300 
createTestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)301         private TestMedia createTestMedia(String localId, String cloudId, String albumId,
302                 String mimeType, int standardMimeTypeExtension, long sizeBytes,
303                 boolean isFavorite) {
304             // Increase generation
305             return new TestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension,
306                     sizeBytes, /* durationMs */ 0, ++mLastSyncGeneration, isFavorite);
307         }
308 
createPlaceholderMedia(String localId, String cloudId)309         private static TestMedia createPlaceholderMedia(String localId, String cloudId) {
310             // Don't increase generation. Used to create a throw-away element used for removal from
311             // |mMedia| or |mDeletedMedia|
312             return new TestMedia(localId, cloudId, 0);
313         }
314 
getCursor( List<TestMedia> mediaList, long generation, String albumId, String[] mimeTypes, long sizeBytes, boolean isDeleted, String pageToken)315         private Cursor getCursor(
316                 List<TestMedia> mediaList,
317                 long generation,
318                 String albumId,
319                 String[] mimeTypes,
320                 long sizeBytes,
321                 boolean isDeleted,
322                 String pageToken) {
323             final MatrixCursor matrix;
324             final int pageSize = 5;
325 
326             if (isDeleted) {
327                 matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION);
328             } else if (!TextUtils.isEmpty(albumId)) {
329                 matrix = new MatrixCursor(ALBUM_MEDIA_PROJECTION);
330             } else {
331                 matrix = new MatrixCursor(MEDIA_PROJECTION);
332             }
333 
334             int page = 0;
335             if (pageToken != null) {
336                 page = Integer.parseInt(pageToken);
337             }
338 
339             // Calculate the starting position: pageSize * pageNumber
340             int startPosition = (pageSize * page);
341             // Calculate the end of the page
342             int endPosition = startPosition + pageSize;
343 
344             for (int i = startPosition; i < endPosition; i++) {
345 
346                 try {
347                     TestMedia media = mediaList.get(i);
348                     if (!TextUtils.isEmpty(albumId)
349                             && matchesFilter(media, albumId, mimeTypes, sizeBytes)) {
350                         matrix.addRow(media.toAlbumMediaArray());
351                     } else if (media.generation > generation
352                             && matchesFilter(media, albumId, mimeTypes, sizeBytes)) {
353                         matrix.addRow(media.toArray(isDeleted));
354                     }
355 
356                 } catch (IndexOutOfBoundsException e) {
357                     // We're at the end of the list, before the end of the page so break the loop.
358                     break;
359                 }
360             }
361 
362             // Set next page token if there is another page.
363             if (mediaList.size() > endPosition) {
364                 mNextPageToken = Integer.valueOf(++page);
365             } else {
366                 mNextPageToken = null;
367             }
368 
369             return matrix;
370         }
371 
getCursor(List<TestAlbum> albumList, String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken)372         private static Cursor getCursor(List<TestAlbum> albumList, String[] mimeTypes,
373                 long sizeBytes, boolean isLocal, String pageToken) {
374             final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION);
375             final int pageSize = 5;
376 
377             int page = 0;
378             if (pageToken != null) {
379                 page = Integer.parseInt(pageToken);
380             }
381 
382             // Calculate the starting position: pageSize * pageNumber
383             int startPosition = (pageSize * page);
384             // Calculate the end of the page
385             int endPosition = startPosition + pageSize;
386 
387 
388             for (int i = startPosition; i < endPosition; i++) {
389 
390                 try {
391                     TestAlbum album = albumList.get(i);
392                     final String[] res = album.toArray(mimeTypes, sizeBytes, isLocal);
393                     if (res != null) {
394                         matrix.addRow(res);
395                     }
396                 } catch (IndexOutOfBoundsException e) {
397                     // We're at the end of the list, before the end of the page so break the loop.
398                     break;
399                 }
400             }
401             return matrix;
402         }
403     }
404 
405     private static class TestMedia {
406         public final String localId;
407         public final String cloudId;
408         public final String albumId;
409         public final String mimeType;
410         public final int standardMimeTypeExtension;
411         public final long sizeBytes;
412         public final long dateTakenMs;
413         public final long durationMs;
414         public final long generation;
415         public final boolean isFavorite;
416 
TestMedia(String localId, String cloudId, long generation)417         public TestMedia(String localId, String cloudId, long generation) {
418             this(localId, cloudId, /* albumId */ null, "image/jpeg",
419                     /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE,
420                     /* sizeBytes */ 4096, /* durationMs */ 0, generation,
421                     /* isFavorite */ false);
422         }
423 
424 
TestMedia(String localId, String cloudId, String albumId)425         public TestMedia(String localId, String cloudId, String albumId) {
426             this(localId, cloudId, /* albumId */ albumId, "image/jpeg",
427                     /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE,
428                     /* sizeBytes */ 4096, /* durationMs */ 0, 0,
429                     /* isFavorite */ false);
430         }
431 
TestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation, boolean isFavorite)432         public TestMedia(String localId, String cloudId, String albumId, String mimeType,
433                 int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation,
434                 boolean isFavorite) {
435             this.localId = localId;
436             this.cloudId = cloudId;
437             this.albumId = albumId;
438             this.mimeType = mimeType;
439             this.standardMimeTypeExtension = standardMimeTypeExtension;
440             this.sizeBytes = sizeBytes;
441             this.dateTakenMs = System.currentTimeMillis();
442             this.durationMs = durationMs;
443             this.generation = generation;
444             this.isFavorite = isFavorite;
445             SystemClock.sleep(1);
446         }
447 
toArray(boolean isDeleted)448         public String[] toArray(boolean isDeleted) {
449             if (isDeleted) {
450                 return new String[] {getId()};
451             }
452 
453             return new String[] {
454                 getId(),
455                 localId == null ? null : "content://media/external/files/" + localId,
456                 mimeType,
457                 String.valueOf(standardMimeTypeExtension),
458                 String.valueOf(dateTakenMs),
459                 String.valueOf(generation),
460                 String.valueOf(sizeBytes),
461                 String.valueOf(durationMs),
462                 String.valueOf(isFavorite ? 1 : 0)
463             };
464         }
465 
toAlbumMediaArray()466         public String[] toAlbumMediaArray() {
467             return new String[] {
468                     getId(),
469                     localId == null ? null : "content://media/external/files/" + localId,
470                     mimeType,
471                     String.valueOf(standardMimeTypeExtension),
472                     String.valueOf(dateTakenMs),
473                     String.valueOf(generation),
474                     String.valueOf(sizeBytes),
475                     String.valueOf(durationMs)
476             };
477         }
478 
479         @Override
equals(Object o)480         public boolean equals(Object o) {
481             if (o == null || !(o instanceof TestMedia)) {
482                 return false;
483             }
484             TestMedia other = (TestMedia) o;
485             return Objects.equals(localId, other.localId) && Objects.equals(cloudId, other.cloudId);
486         }
487 
488         @Override
hashCode()489         public int hashCode() {
490             return Objects.hash(localId, cloudId);
491         }
492 
getId()493         private String getId() {
494             return cloudId == null ? localId : cloudId;
495         }
496     }
497 
498     private static class TestAlbum {
499         public final String id;
500         private final List<TestMedia> media;
501 
TestAlbum(String id, List<TestMedia> media)502         public TestAlbum(String id, List<TestMedia> media) {
503             this.id = id;
504             this.media = media;
505         }
506 
toArray(String[] mimeTypes, long sizeBytes, boolean isLocal)507         public String[] toArray(String[] mimeTypes, long sizeBytes, boolean isLocal) {
508             long mediaCount = 0;
509             String mediaCoverId = null;
510             long dateTakenMs = 0;
511 
512             for (TestMedia m : media) {
513                 if (matchesFilter(m, id, mimeTypes, sizeBytes)) {
514                     if (mediaCount++ == 0) {
515                         mediaCoverId = m.getId();
516                         dateTakenMs = m.dateTakenMs;
517                     }
518                 }
519             }
520 
521             if (mediaCount == 0) {
522                 return null;
523             }
524 
525             return new String[] {
526                 id,
527                 String.valueOf(dateTakenMs),
528                 /* displayName */ id,
529                 mediaCoverId,
530                 String.valueOf(mediaCount),
531                 isLocal ? LocalProvider.AUTHORITY : null
532             };
533         }
534 
535         @Override
equals(Object o)536         public boolean equals(Object o) {
537             if (o == null || !(o instanceof TestAlbum)) {
538                 return false;
539             }
540 
541             TestAlbum other = (TestAlbum) o;
542             return Objects.equals(id, other.id);
543         }
544 
545         @Override
hashCode()546         public int hashCode() {
547             return Objects.hash(id);
548         }
549     }
550 
matchesFilter(TestMedia media, String albumId, String[] mimeTypes, long sizeBytes)551     private static boolean matchesFilter(TestMedia media, String albumId, String[] mimeTypes,
552             long sizeBytes) {
553         if (!Objects.equals(albumId, STRING_DEFAULT) && !Objects.equals(albumId, media.albumId)) {
554             return false;
555         }
556 
557         if (mimeTypes != null) {
558             boolean matchesMimeType = false;
559             for (String m : mimeTypes) {
560                 if (m != null && media.mimeType.startsWith(m)) {
561                     matchesMimeType = true;
562                     break;
563                 }
564             }
565 
566             if (!matchesMimeType) {
567                 return false;
568             }
569         }
570 
571         if (sizeBytes != LONG_DEFAULT && media.sizeBytes > sizeBytes) {
572             return false;
573         }
574 
575         return true;
576     }
577 
getMediaGenerator(String authority)578     public static MediaGenerator getMediaGenerator(String authority) {
579         MediaGenerator generator = sMediaGeneratorMap.get(authority);
580         if (generator == null) {
581             generator = new MediaGenerator();
582             sMediaGeneratorMap.put(authority, generator);
583         }
584         return generator;
585     }
586 }
587