• 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_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 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
26 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
27 
28 import android.content.ContentResolver;
29 import android.content.Intent;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.os.Bundle;
33 import android.os.SystemClock;
34 import android.provider.CloudMediaProvider;
35 import android.provider.CloudMediaProviderContract;
36 import android.text.TextUtils;
37 import android.util.Log;
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.DISPLAY_NAME,
79         AlbumColumns.DATE_TAKEN_MILLIS,
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 mimeType, long sizeBytes)99         public Cursor getMedia(long generation, String albumId, String mimeType, long sizeBytes) {
100             final Cursor cursor = getCursor(mMedia, generation, albumId, mimeType, sizeBytes,
101                     /* isDeleted */ false);
102 
103             if (mCursorExtra != null) {
104                 cursor.setExtras(mCursorExtra);
105             } else {
106                 cursor.setExtras(buildCursorExtras(mCollectionId, generation > 0, albumId != null));
107             }
108 
109             if (--mCursorExtraQueryCount == 0) {
110                 clearCursorExtras();
111             }
112             return cursor;
113         }
114 
getAlbums(String mimeType, long sizeBytes, boolean isLocal)115         public Cursor getAlbums(String mimeType, long sizeBytes, boolean isLocal) {
116             final Cursor cursor = getCursor(mAlbums, mimeType, sizeBytes, isLocal);
117 
118             if (mCursorExtra != null) {
119                 cursor.setExtras(mCursorExtra);
120             } else {
121                 cursor.setExtras(buildCursorExtras(mCollectionId, false, false));
122             }
123 
124             if (--mCursorExtraQueryCount == 0) {
125                 clearCursorExtras();
126             }
127             return cursor;
128         }
129 
130         // TODO(b/214592293): Add pagination support for testing purposes.
getDeletedMedia(long generation)131         public Cursor getDeletedMedia(long generation) {
132             final Cursor cursor = getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT,
133                     /* mimeType */ STRING_DEFAULT, /* sizeBytes */ LONG_DEFAULT,
134                     /* isDeleted */ true);
135 
136             if (mCursorExtra != null) {
137                 cursor.setExtras(mCursorExtra);
138             } else {
139                 cursor.setExtras(buildCursorExtras(mCollectionId, generation > 0, false));
140             }
141 
142             if (--mCursorExtraQueryCount == 0) {
143                 clearCursorExtras();
144             }
145             return cursor;
146         }
147 
getMediaCollectionInfo()148         public Bundle getMediaCollectionInfo() {
149             Bundle bundle = new Bundle();
150             bundle.putString(MediaCollectionInfo.MEDIA_COLLECTION_ID, mCollectionId);
151             bundle.putLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, mLastSyncGeneration);
152             bundle.putString(MediaCollectionInfo.ACCOUNT_NAME, mAccountName);
153             bundle.putParcelable(MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT,
154                     mAccountConfigurationIntent);
155 
156             return bundle;
157         }
158 
setAccountInfo(String accountName, Intent configIntent)159         public void setAccountInfo(String accountName, Intent configIntent) {
160             mAccountName = accountName;
161             mAccountConfigurationIntent = configIntent;
162         }
163 
clearCursorExtras()164         public void clearCursorExtras() {
165             mCursorExtra = null;
166         }
167 
setNextCursorExtras(int queryCount, String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumId)168         public void setNextCursorExtras(int queryCount, String mediaCollectionId,
169                 boolean honoredSyncGeneration, boolean honoredAlbumId) {
170             mCursorExtraQueryCount = queryCount;
171             mCursorExtra = buildCursorExtras(mediaCollectionId, honoredSyncGeneration,
172                     honoredAlbumId);
173         }
174 
buildCursorExtras(String mediaCollectionId, boolean honoredSyncGeneration, boolean honoredAlbumdId)175         public Bundle buildCursorExtras(String mediaCollectionId, boolean honoredSyncGeneration,
176                 boolean honoredAlbumdId) {
177             final ArrayList<String> honoredArgs = new ArrayList<>();
178             if (honoredSyncGeneration) {
179                 honoredArgs.add(EXTRA_SYNC_GENERATION);
180             }
181             if (honoredAlbumdId) {
182                 honoredArgs.add(EXTRA_ALBUM_ID);
183             }
184 
185             final Bundle bundle = new Bundle();
186             bundle.putString(CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID,
187                     mediaCollectionId);
188             bundle.putStringArrayList(ContentResolver.EXTRA_HONORED_ARGS, honoredArgs);
189 
190             return bundle;
191         }
192 
addMedia(String localId, String cloudId)193         public void addMedia(String localId, String cloudId) {
194             mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
195             mMedia.add(0, createTestMedia(localId, cloudId));
196         }
197 
addAlbumMedia(String localId, String cloudId, String albumId)198         public void addAlbumMedia(String localId, String cloudId, String albumId) {
199             mMedia.add(0, createTestAlbumMedia(localId, cloudId, albumId));
200         }
201 
addMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)202         public void addMedia(String localId, String cloudId, String albumId, String mimeType,
203                 int standardMimeTypeExtension, long sizeBytes, boolean isFavorite) {
204             mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
205             mMedia.add(0,
206                     createTestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension,
207                             sizeBytes, isFavorite));
208         }
209 
deleteMedia(String localId, String cloudId)210         public void deleteMedia(String localId, String cloudId) {
211             if (mMedia.remove(createPlaceholderMedia(localId, cloudId))) {
212                 mDeletedMedia.add(createTestMedia(localId, cloudId));
213             }
214         }
215 
createAlbum(String id)216         public void createAlbum(String id) {
217             mAlbums.add(createTestAlbum(id));
218         }
219 
resetAll()220         public void resetAll() {
221             mMedia.clear();
222             mDeletedMedia.clear();
223             mAlbums.clear();
224             clearCursorExtras();
225         }
226 
setMediaCollectionId(String id)227         public void setMediaCollectionId(String id) {
228             mCollectionId = id;
229         }
230 
getCount()231         public long getCount() {
232             return mMedia.size();
233         }
234 
createTestAlbum(String id)235         private TestAlbum createTestAlbum(String id) {
236             return new TestAlbum(id, mMedia);
237         }
238 
createTestMedia(String localId, String cloudId)239         private TestMedia createTestMedia(String localId, String cloudId) {
240             // Increase generation
241             return new TestMedia(localId, cloudId, ++mLastSyncGeneration);
242         }
createTestAlbumMedia(String localId, String cloudId, String albumId)243         private TestMedia createTestAlbumMedia(String localId, String cloudId, String albumId) {
244             // Increase generation
245             return new TestMedia(localId, cloudId, albumId);
246         }
247 
createTestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, boolean isFavorite)248         private TestMedia createTestMedia(String localId, String cloudId, String albumId,
249                 String mimeType, int standardMimeTypeExtension, long sizeBytes,
250                 boolean isFavorite) {
251             // Increase generation
252             return new TestMedia(localId, cloudId, albumId, mimeType, standardMimeTypeExtension,
253                     sizeBytes, /* durationMs */ 0, ++mLastSyncGeneration, isFavorite);
254         }
255 
createPlaceholderMedia(String localId, String cloudId)256         private static TestMedia createPlaceholderMedia(String localId, String cloudId) {
257             // Don't increase generation. Used to create a throw-away element used for removal from
258             // |mMedia| or |mDeletedMedia|
259             return new TestMedia(localId, cloudId, 0);
260         }
261 
getCursor(List<TestMedia> mediaList, long generation, String albumId, String mimeType, long sizeBytes, boolean isDeleted)262         private static Cursor getCursor(List<TestMedia> mediaList, long generation,
263                 String albumId, String mimeType, long sizeBytes, boolean isDeleted) {
264             final MatrixCursor matrix;
265             if (isDeleted) {
266                 matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION);
267             } else if(!TextUtils.isEmpty(albumId)) {
268                 matrix = new MatrixCursor(ALBUM_MEDIA_PROJECTION);
269             } else {
270                 matrix = new MatrixCursor(MEDIA_PROJECTION);
271             }
272 
273             for (TestMedia media : mediaList) {
274                 if (!TextUtils.isEmpty(albumId) && matchesFilter(media,
275                         albumId, mimeType, sizeBytes)) {
276                     matrix.addRow(media.toAlbumMediaArray());
277                 } else if (media.generation > generation
278                         && matchesFilter(media, albumId, mimeType, sizeBytes)) {
279                     matrix.addRow(media.toArray(isDeleted));
280                 }
281             }
282             return matrix;
283         }
284 
getCursor(List<TestAlbum> albumList, String mimeType, long sizeBytes, boolean isLocal)285         private static Cursor getCursor(List<TestAlbum> albumList, String mimeType, long sizeBytes,
286                 boolean isLocal) {
287             final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION);
288 
289             for (TestAlbum album : albumList) {
290                 final String[] res = album.toArray(mimeType, sizeBytes, isLocal);
291                 if (res != null) {
292                     matrix.addRow(res);
293                 }
294             }
295             return matrix;
296         }
297     }
298 
299     private static class TestMedia {
300         public final String localId;
301         public final String cloudId;
302         public final String albumId;
303         public final String mimeType;
304         public final int standardMimeTypeExtension;
305         public final long sizeBytes;
306         public final long dateTakenMs;
307         public final long durationMs;
308         public final long generation;
309         public final boolean isFavorite;
310 
TestMedia(String localId, String cloudId, long generation)311         public TestMedia(String localId, String cloudId, long generation) {
312             this(localId, cloudId, /* albumId */ null, "image/jpeg",
313                     /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE,
314                     /* sizeBytes */ 4096, /* durationMs */ 0, generation,
315                     /* isFavorite */ false);
316         }
317 
318 
TestMedia(String localId, String cloudId, String albumId)319         public TestMedia(String localId, String cloudId, String albumId) {
320             this(localId, cloudId, /* albumId */ albumId, "image/jpeg",
321                     /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE,
322                     /* sizeBytes */ 4096, /* durationMs */ 0, 0,
323                     /* isFavorite */ false);
324         }
325 
TestMedia(String localId, String cloudId, String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation, boolean isFavorite)326         public TestMedia(String localId, String cloudId, String albumId, String mimeType,
327                 int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation,
328                 boolean isFavorite) {
329             this.localId = localId;
330             this.cloudId = cloudId;
331             this.albumId = albumId;
332             this.mimeType = mimeType;
333             this.standardMimeTypeExtension = standardMimeTypeExtension;
334             this.sizeBytes = sizeBytes;
335             this.dateTakenMs = System.currentTimeMillis();
336             this.durationMs = durationMs;
337             this.generation = generation;
338             this.isFavorite = isFavorite;
339             SystemClock.sleep(1);
340         }
341 
toArray(boolean isDeleted)342         public String[] toArray(boolean isDeleted) {
343             if (isDeleted) {
344                 return new String[] {getId()};
345             }
346 
347             return new String[] {
348                 getId(),
349                 localId == null ? null : "content://media/external/files/" + localId,
350                 mimeType,
351                 String.valueOf(standardMimeTypeExtension),
352                 String.valueOf(dateTakenMs),
353                 String.valueOf(generation),
354                 String.valueOf(sizeBytes),
355                 String.valueOf(durationMs),
356                 String.valueOf(isFavorite ? 1 : 0)
357             };
358         }
359 
toAlbumMediaArray()360         public String[] toAlbumMediaArray() {
361             return new String[] {
362                     getId(),
363                     localId == null ? null : "content://media/external/files/" + localId,
364                     mimeType,
365                     String.valueOf(standardMimeTypeExtension),
366                     String.valueOf(dateTakenMs),
367                     String.valueOf(generation),
368                     String.valueOf(sizeBytes),
369                     String.valueOf(durationMs)
370             };
371         }
372 
373         @Override
equals(Object o)374         public boolean equals(Object o) {
375             if (o == null || !(o instanceof TestMedia)) {
376                 return false;
377             }
378             TestMedia other = (TestMedia) o;
379             return Objects.equals(localId, other.localId) && Objects.equals(cloudId, other.cloudId);
380         }
381 
382         @Override
hashCode()383         public int hashCode() {
384             return Objects.hash(localId, cloudId);
385         }
386 
getId()387         private String getId() {
388             return cloudId == null ? localId : cloudId;
389         }
390     }
391 
392     private static class TestAlbum {
393         public final String id;
394         private final List<TestMedia> media;
395 
TestAlbum(String id, List<TestMedia> media)396         public TestAlbum(String id, List<TestMedia> media) {
397             this.id = id;
398             this.media = media;
399         }
400 
toArray(String mimeType, long sizeBytes, boolean isLocal)401         public String[] toArray(String mimeType, long sizeBytes, boolean isLocal) {
402             long mediaCount = 0;
403             String mediaCoverId = null;
404             long dateTakenMs = 0;
405 
406             for (TestMedia m : media) {
407                 if (matchesFilter(m, id, mimeType, sizeBytes)) {
408                     if (mediaCount++ == 0) {
409                         mediaCoverId = m.getId();
410                         dateTakenMs = m.dateTakenMs;
411                     }
412                 }
413             }
414 
415             if (mediaCount == 0) {
416                 return null;
417             }
418 
419             return new String[] {
420                 id,
421                 mediaCoverId,
422                 /* displayName */ id,
423                 String.valueOf(dateTakenMs),
424                 String.valueOf(mediaCount),
425                 isLocal ? LocalProvider.AUTHORITY : null
426             };
427         }
428 
429         @Override
equals(Object o)430         public boolean equals(Object o) {
431             if (o == null || !(o instanceof TestAlbum)) {
432                 return false;
433             }
434 
435             TestAlbum other = (TestAlbum) o;
436             return Objects.equals(id, other.id);
437         }
438 
439         @Override
hashCode()440         public int hashCode() {
441             return Objects.hash(id);
442         }
443     }
444 
matchesFilter(TestMedia media, String albumId, String mimeType, long sizeBytes)445     private static boolean matchesFilter(TestMedia media, String albumId, String mimeType,
446             long sizeBytes) {
447         if (!Objects.equals(albumId, STRING_DEFAULT) && !Objects.equals(albumId, media.albumId)) {
448             return false;
449         }
450         if (!Objects.equals(mimeType, STRING_DEFAULT) && !media.mimeType.startsWith(mimeType)) {
451             return false;
452         }
453         if (sizeBytes != LONG_DEFAULT && media.sizeBytes > sizeBytes) {
454             return false;
455         }
456 
457         return true;
458     }
459 
getMediaGenerator(String authority)460     public static MediaGenerator getMediaGenerator(String authority) {
461         MediaGenerator generator = sMediaGeneratorMap.get(authority);
462         if (generator == null) {
463             generator = new MediaGenerator();
464             sMediaGeneratorMap.put(authority, generator);
465         }
466         return generator;
467     }
468 }
469