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