• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.photopicker.data
18 
19 import android.database.Cursor
20 import android.database.MatrixCursor
21 import android.net.Uri
22 import android.os.Bundle
23 import android.os.CancellationSignal
24 import android.test.mock.MockContentProvider
25 import androidx.core.os.bundleOf
26 import com.android.photopicker.data.model.CategoryType
27 import com.android.photopicker.data.model.CollectionInfo
28 import com.android.photopicker.data.model.Group
29 import com.android.photopicker.data.model.Icon
30 import com.android.photopicker.data.model.Media
31 import com.android.photopicker.data.model.MediaSource
32 import com.android.photopicker.data.model.Provider
33 import com.android.photopicker.features.search.model.SearchSuggestion
34 import com.android.photopicker.features.search.model.SearchSuggestionType
35 import java.util.UUID
36 import java.util.stream.Collectors
37 
38 /**
39  * A test utility that provides implementation for some MediaProvider queries.
40  *
41  * This will be used to wrap [ContentResolver] to intercept calls to it and re-route them to the
42  * internal mock this class holds.
43  *
44  * All not overridden / unimplemented operations will throw [UnsupportedOperationException].
45  */
46 val DEFAULT_PROVIDERS: List<Provider> =
47     listOf(
48         Provider(
49             authority = "test_authority",
50             mediaSource = MediaSource.LOCAL,
51             uid = 0,
52             displayName = "Test app",
53         )
54     )
55 
56 val DEFAULT_COLLECTION_INFO: List<CollectionInfo> =
57     listOf(
58         CollectionInfo(
59             authority = "test_authority",
60             collectionId = "1",
61             accountName = "default@test.com",
62         )
63     )
64 
65 val DEFAULT_MEDIA: List<Media> =
66     listOf(
67         createMediaImage(10),
68         createMediaImage(11),
69         createMediaImage(12),
70         createMediaImage(13),
71         createMediaImage(14),
72     )
73 
74 val DEFAULT_ALBUMS: List<Group.Album> =
75     listOf(createAlbum("Favorites"), createAlbum("Downloads"), createAlbum("CloudAlbum"))
76 
77 val DEFAULT_ALBUM_NAME = "album_id"
78 
79 val DEFAULT_ALBUM_MEDIA: Map<String, List<Media>> = mapOf(DEFAULT_ALBUM_NAME to DEFAULT_MEDIA)
80 
81 val DEFAULT_SEARCH_REQUEST_ID: Int = 100
82 
83 val DEFAULT_SEARCH_SUGGESTIONS: List<SearchSuggestion> =
84     listOf(
85         SearchSuggestion(
86             mediaSetId = null,
87             authority = null,
88             type = SearchSuggestionType.HISTORY,
89             displayText = "Text",
90             icon = null,
91         ),
92         SearchSuggestion(
93             mediaSetId = "media-set-id-1",
94             authority = "cloud.provider",
95             type = SearchSuggestionType.FACE,
96             displayText = null,
97             icon = Icon(Uri.parse("content://cloud.provider/1234"), MediaSource.LOCAL),
98         ),
99         SearchSuggestion(
100             mediaSetId = "media-set-id-1",
101             authority = "local-provider",
102             type = SearchSuggestionType.TEXT,
103             displayText = "Text",
104             icon = null,
105         ),
106     )
107 
108 val DEFAULT_CATEGORY: Group.Category =
109     createCategory(CategoryType.PEOPLE_AND_PETS, DEFAULT_PROVIDERS[0].authority)
110 
111 val DEFAULT_CATEGORIES_AND_ALBUMS: List<Group> =
112     listOf(
113         createAlbum("Favorites"),
114         createAlbum("Downloads"),
115         DEFAULT_CATEGORY,
116         createAlbum("CloudAlbum"),
117     )
118 
119 val DEFAULT_MEDIA_SETS: List<Group.MediaSet> =
120     listOf(createMediaSet("1"), createMediaSet("2"), createMediaSet("3"))
121 
122 fun createMediaImage(pickerId: Long): Media {
123     return Media.Image(
124         mediaId = UUID.randomUUID().toString(),
125         pickerId = pickerId,
126         authority = "authority",
127         mediaSource = MediaSource.LOCAL,
128         mediaUri = Uri.parse("content://media/picker/authority/media/$pickerId"),
129         glideLoadableUri = Uri.parse("content://authority/media/$pickerId"),
130         dateTakenMillisLong = Long.MAX_VALUE,
131         sizeInBytes = 10,
132         mimeType = "image/*",
133         standardMimeTypeExtension = 0,
134     )
135 }
136 
createAlbumnull137 fun createAlbum(albumId: String): Group.Album {
138     return Group.Album(
139         id = albumId,
140         pickerId = albumId.hashCode().toLong(),
141         authority = DEFAULT_PROVIDERS[0].authority,
142         dateTakenMillisLong = Long.MAX_VALUE,
143         displayName = albumId,
144         coverUri = Uri.parse("content://test_authority/$albumId"),
145         coverMediaSource = DEFAULT_PROVIDERS[0].mediaSource,
146     )
147 }
148 
createCategorynull149 fun createCategory(type: CategoryType, authority: String): Group.Category {
150     return Group.Category(
151         id = "test_id_" + type.name,
152         pickerId = 0,
153         authority = authority,
154         displayName = type.name,
155         categoryType = type,
156         icons = listOf(Icon(Uri.parse("content://test_authority/id"), MediaSource.LOCAL)),
157         isLeafCategory = true,
158     )
159 }
160 
createMediaSetnull161 fun createMediaSet(mediaSetId: String): Group.MediaSet {
162     return Group.MediaSet(
163         id = mediaSetId,
164         pickerId = mediaSetId.hashCode().toLong(),
165         authority = DEFAULT_PROVIDERS[0].authority,
166         displayName = mediaSetId,
167         icon = Icon(Uri.parse("content://test_authority/$mediaSetId"), MediaSource.LOCAL),
168     )
169 }
170 
171 class TestMediaProvider(
172     var providers: List<Provider> = DEFAULT_PROVIDERS,
173     var collectionInfos: List<CollectionInfo> = DEFAULT_COLLECTION_INFO,
174     var media: List<Media> = DEFAULT_MEDIA,
175     var albums: List<Group.Album> = DEFAULT_ALBUMS,
176     var albumMedia: Map<String, List<Media>> = DEFAULT_ALBUM_MEDIA,
177     var searchRequestId: Int = DEFAULT_SEARCH_REQUEST_ID,
178     var searchSuggestions: List<SearchSuggestion> = DEFAULT_SEARCH_SUGGESTIONS,
179     var searchProviders: List<Provider>? = DEFAULT_PROVIDERS,
180     var parentCategory: Group.Category = DEFAULT_CATEGORY,
181     var categoriesAndAlbums: List<Group> = DEFAULT_CATEGORIES_AND_ALBUMS,
182     var mediaSets: List<Group.MediaSet> = DEFAULT_MEDIA_SETS,
183 ) : MockContentProvider() {
184     var lastRefreshMediaRequest: Bundle? = null
185     var TEST_GRANTS_COUNT = 2
186 
querynull187     override fun query(
188         uri: Uri,
189         projection: Array<String>?,
190         queryArgs: Bundle?,
191         cancellationSignal: CancellationSignal?,
192     ): Cursor? {
193         return when (uri.lastPathSegment) {
194             AVAILABLE_PROVIDERS_PATH_SEGMENT -> getAvailableProviders()
195             COLLECTION_INFO_SEGMENT -> getCollectionInfo()
196             MEDIA_PATH_SEGMENT -> getMedia()
197             ALBUM_PATH_SEGMENT -> getAlbums()
198             MEDIA_GRANTS_COUNT_PATH_SEGMENT -> fetchMediaGrantsCount()
199             PRE_SELECTION_URI_PATH_SEGMENT -> fetchFilteredMedia(queryArgs)
200             SEARCH_SUGGESTIONS_PATH_SEGMENT -> getSearchSuggestions()
201             CATEGORIES_PATH_SEGMENT -> getCategoriesAndAlbums()
202             MEDIA_SETS_PATH_SEGMENT -> getMediaSets()
203             MEDIA_SET_CONTENTS_PATH_SEGMENT -> getMedia()
204             else -> {
205                 val pathSegments: MutableList<String> = uri.getPathSegments()
206                 if (pathSegments.size == 4 && pathSegments[2].equals(ALBUM_PATH_SEGMENT)) {
207                     // Album media query
208                     return getAlbumMedia(pathSegments[3])
209                 } else if (
210                     pathSegments.size == 4 && pathSegments[2].equals(SEARCH_MEDIA_PATH_SEGMENT)
211                 ) {
212                     // Search results media query
213                     return getMedia()
214                 } else {
215                     throw UnsupportedOperationException("Could not recognize uri $uri")
216                 }
217             }
218         }
219     }
220 
callnull221     override fun call(authority: String, method: String, arg: String?, extras: Bundle?): Bundle? {
222         return when (method) {
223             MediaProviderClient.MEDIA_INIT_CALL_METHOD -> {
224                 initMedia(extras)
225                 null
226             }
227             MediaProviderClient.SEARCH_REQUEST_INIT_CALL_METHOD -> {
228                 bundleOf(MediaProviderClient.SEARCH_REQUEST_ID to searchRequestId)
229             }
230             MediaProviderClient.GET_SEARCH_PROVIDERS_CALL_METHOD ->
231                 bundleOf(
232                     MediaProviderClient.SEARCH_PROVIDER_AUTHORITIES to
233                         if (searchProviders == null) null
234                         else
235                             arrayListOf<String>().apply {
236                                 searchProviders?.map { it.authority }?.toCollection(this)
237                             }
238                 )
239             else -> throw UnsupportedOperationException("Could not recognize method $method")
240         }
241     }
242 
243     /** Returns a [Cursor] with the providers currently in the [providers] list. */
getAvailableProvidersnull244     private fun getAvailableProviders(): Cursor {
245         val cursor =
246             MatrixCursor(
247                 arrayOf(
248                     MediaProviderClient.AvailableProviderResponse.AUTHORITY.key,
249                     MediaProviderClient.AvailableProviderResponse.MEDIA_SOURCE.key,
250                     MediaProviderClient.AvailableProviderResponse.UID.key,
251                     MediaProviderClient.AvailableProviderResponse.DISPLAY_NAME.key,
252                 )
253             )
254         providers.forEach { provider ->
255             cursor.addRow(
256                 arrayOf(
257                     provider.authority,
258                     provider.mediaSource.name,
259                     provider.uid.toString(),
260                     provider.displayName,
261                 )
262             )
263         }
264         return cursor
265     }
266 
getCollectionInfonull267     private fun getCollectionInfo(): Cursor {
268         val cursor =
269             MatrixCursor(
270                 arrayOf(
271                     MediaProviderClient.CollectionInfoResponse.AUTHORITY.key,
272                     MediaProviderClient.CollectionInfoResponse.COLLECTION_ID.key,
273                     MediaProviderClient.CollectionInfoResponse.ACCOUNT_NAME.key,
274                 )
275             )
276         cursor.setExtras(Bundle())
277         collectionInfos.forEach { collectionInfo ->
278             cursor.addRow(
279                 arrayOf(
280                     collectionInfo.authority,
281                     collectionInfo.collectionId,
282                     collectionInfo.accountName,
283                 )
284             )
285             cursor
286                 .getExtras()
287                 .putParcelable(collectionInfo.authority, collectionInfo.accountConfigurationIntent)
288         }
289         return cursor
290     }
291 
getMedianull292     private fun getMedia(mediaItems: List<Media> = media): Cursor {
293         val cursor =
294             MatrixCursor(
295                 arrayOf(
296                     MediaProviderClient.MediaResponse.MEDIA_ID.key,
297                     MediaProviderClient.MediaResponse.PICKER_ID.key,
298                     MediaProviderClient.MediaResponse.AUTHORITY.key,
299                     MediaProviderClient.MediaResponse.MEDIA_SOURCE.key,
300                     MediaProviderClient.MediaResponse.MEDIA_URI.key,
301                     MediaProviderClient.MediaResponse.LOADABLE_URI.key,
302                     MediaProviderClient.MediaResponse.DATE_TAKEN.key,
303                     MediaProviderClient.MediaResponse.SIZE.key,
304                     MediaProviderClient.MediaResponse.MIME_TYPE.key,
305                     MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key,
306                     MediaProviderClient.MediaResponse.DURATION.key,
307                     MediaProviderClient.MediaResponse.IS_PRE_GRANTED.key,
308                 )
309             )
310         mediaItems.forEach { mediaItem ->
311             cursor.addRow(
312                 arrayOf(
313                     mediaItem.mediaId,
314                     mediaItem.pickerId.toString(),
315                     mediaItem.authority,
316                     mediaItem.mediaSource.toString(),
317                     mediaItem.mediaUri.toString(),
318                     mediaItem.glideLoadableUri.toString(),
319                     mediaItem.dateTakenMillisLong.toString(),
320                     mediaItem.sizeInBytes.toString(),
321                     mediaItem.mimeType,
322                     mediaItem.standardMimeTypeExtension.toString(),
323                     if (mediaItem is Media.Video) mediaItem.duration else "0",
324                     if (mediaItem.isPreGranted) 1 else 0,
325                 )
326             )
327         }
328         return cursor
329     }
330 
fetchFilteredMedianull331     private fun fetchFilteredMedia(queryArgs: Bundle?, mediaItems: List<Media> = media): Cursor {
332         val ids =
333             queryArgs
334                 ?.getStringArrayList("pre_selection_uris")
335                 ?.stream()
336                 ?.map { it -> Uri.parse(it).lastPathSegment }
337                 ?.collect(Collectors.toList())
338         val cursor =
339             MatrixCursor(
340                 arrayOf(
341                     MediaProviderClient.MediaResponse.MEDIA_ID.key,
342                     MediaProviderClient.MediaResponse.PICKER_ID.key,
343                     MediaProviderClient.MediaResponse.AUTHORITY.key,
344                     MediaProviderClient.MediaResponse.MEDIA_SOURCE.key,
345                     MediaProviderClient.MediaResponse.MEDIA_URI.key,
346                     MediaProviderClient.MediaResponse.LOADABLE_URI.key,
347                     MediaProviderClient.MediaResponse.DATE_TAKEN.key,
348                     MediaProviderClient.MediaResponse.SIZE.key,
349                     MediaProviderClient.MediaResponse.MIME_TYPE.key,
350                     MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key,
351                     MediaProviderClient.MediaResponse.DURATION.key,
352                     MediaProviderClient.MediaResponse.IS_PRE_GRANTED.key,
353                 )
354             )
355         mediaItems.forEach { mediaItem ->
356             if (ids != null) {
357                 if (mediaItem.mediaId in ids) {
358                     cursor.addRow(
359                         arrayOf(
360                             mediaItem.mediaId,
361                             mediaItem.pickerId.toString(),
362                             mediaItem.authority,
363                             mediaItem.mediaSource.toString(),
364                             mediaItem.mediaUri.toString(),
365                             mediaItem.glideLoadableUri.toString(),
366                             mediaItem.dateTakenMillisLong.toString(),
367                             mediaItem.sizeInBytes.toString(),
368                             mediaItem.mimeType,
369                             mediaItem.standardMimeTypeExtension.toString(),
370                             if (mediaItem is Media.Video) mediaItem.duration else "0",
371                             if (mediaItem.isPreGranted) 1 else 0,
372                         )
373                     )
374                 }
375             }
376         }
377         return cursor
378     }
379 
getAlbumsnull380     private fun getAlbums(): Cursor {
381         val cursor =
382             MatrixCursor(
383                 arrayOf(
384                     MediaProviderClient.AlbumResponse.ALBUM_ID.key,
385                     MediaProviderClient.AlbumResponse.PICKER_ID.key,
386                     MediaProviderClient.AlbumResponse.AUTHORITY.key,
387                     MediaProviderClient.AlbumResponse.DATE_TAKEN.key,
388                     MediaProviderClient.AlbumResponse.ALBUM_NAME.key,
389                     MediaProviderClient.AlbumResponse.UNWRAPPED_COVER_URI.key,
390                     MediaProviderClient.AlbumResponse.COVER_MEDIA_SOURCE.key,
391                 )
392             )
393         albums.forEach { album ->
394             cursor.addRow(
395                 arrayOf(
396                     album.id,
397                     album.pickerId.toString(),
398                     album.authority,
399                     album.dateTakenMillisLong.toString(),
400                     album.displayName,
401                     album.coverUri.toString(),
402                     album.coverMediaSource.toString(),
403                 )
404             )
405         }
406         return cursor
407     }
408 
fetchMediaGrantsCountnull409     private fun fetchMediaGrantsCount(): Cursor {
410         val cursor = MatrixCursor(arrayOf("grants_count"))
411         cursor.addRow(arrayOf(TEST_GRANTS_COUNT))
412         return cursor
413     }
414 
getAlbumMedianull415     private fun getAlbumMedia(albumId: String): Cursor? {
416         return getMedia(albumMedia.getOrDefault(albumId, emptyList()))
417     }
418 
initMedianull419     private fun initMedia(extras: Bundle?) {
420         lastRefreshMediaRequest = extras
421     }
422 
getSearchSuggestionsnull423     private fun getSearchSuggestions(): Cursor {
424         val cursor =
425             MatrixCursor(
426                 arrayOf(
427                     MediaProviderClient.SearchSuggestionsResponse.AUTHORITY.key,
428                     MediaProviderClient.SearchSuggestionsResponse.MEDIA_SET_ID.key,
429                     MediaProviderClient.SearchSuggestionsResponse.SEARCH_TEXT.key,
430                     MediaProviderClient.SearchSuggestionsResponse.COVER_MEDIA_URI.key,
431                     MediaProviderClient.SearchSuggestionsResponse.SUGGESTION_TYPE.key,
432                 )
433             )
434 
435         searchSuggestions.forEach { suggestion ->
436             cursor.addRow(
437                 arrayOf(
438                     suggestion.authority,
439                     suggestion.mediaSetId,
440                     suggestion.displayText,
441                     suggestion.icon,
442                     suggestion.type.key,
443                 )
444             )
445         }
446         return cursor
447     }
448 
getCategoriesAndAlbumsnull449     private fun getCategoriesAndAlbums(): Cursor {
450         val cursor =
451             MatrixCursor(
452                 arrayOf(
453                     MediaProviderClient.GroupResponse.MEDIA_GROUP.key,
454                     MediaProviderClient.GroupResponse.GROUP_ID.key,
455                     MediaProviderClient.GroupResponse.PICKER_ID.key,
456                     MediaProviderClient.GroupResponse.DISPLAY_NAME.key,
457                     MediaProviderClient.GroupResponse.AUTHORITY.key,
458                     MediaProviderClient.GroupResponse.UNWRAPPED_COVER_URI.key,
459                     MediaProviderClient.GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_1.key,
460                     MediaProviderClient.GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_2.key,
461                     MediaProviderClient.GroupResponse.ADDITIONAL_UNWRAPPED_COVER_URI_3.key,
462                     MediaProviderClient.GroupResponse.CATEGORY_TYPE.key,
463                     MediaProviderClient.GroupResponse.IS_LEAF_CATEGORY.key,
464                 )
465             )
466         categoriesAndAlbums.forEach { group ->
467             when (group) {
468                 is Group.Album ->
469                     cursor.addRow(
470                         arrayOf(
471                             MediaProviderClient.GroupType.ALBUM.name,
472                             group.id,
473                             group.pickerId.toString(),
474                             group.displayName,
475                             group.authority,
476                             group.coverUri.toString(),
477                             /* additional uri */ null,
478                             /* additional uri */ null,
479                             /* additional uri */ null,
480                             /* category type */ null,
481                             /* is leaf category */ null,
482                         )
483                     )
484                 is Group.Category ->
485                     cursor.addRow(
486                         arrayOf(
487                             MediaProviderClient.GroupType.CATEGORY.name,
488                             group.id,
489                             group.pickerId.toString(),
490                             group.displayName,
491                             group.authority,
492                             group.icons.getOrNull(0)?.getLoadableUri()?.toString(),
493                             group.icons.getOrNull(1)?.getLoadableUri()?.toString(),
494                             group.icons.getOrNull(2)?.getLoadableUri()?.toString(),
495                             group.icons.getOrNull(3)?.getLoadableUri()?.toString(),
496                             group.categoryType.key,
497                             if (group.isLeafCategory) 1 else null,
498                         )
499                     )
500                 else -> {}
501             }
502         }
503         return cursor
504     }
505 
getMediaSetsnull506     private fun getMediaSets(): Cursor {
507         val cursor =
508             MatrixCursor(
509                 arrayOf(
510                     MediaProviderClient.GroupResponse.GROUP_ID.key,
511                     MediaProviderClient.GroupResponse.PICKER_ID.key,
512                     MediaProviderClient.GroupResponse.DISPLAY_NAME.key,
513                     MediaProviderClient.GroupResponse.AUTHORITY.key,
514                     MediaProviderClient.GroupResponse.UNWRAPPED_COVER_URI.key,
515                 )
516             )
517         mediaSets.forEach {
518             cursor.addRow(
519                 arrayOf(
520                     it.id,
521                     it.pickerId.toString(),
522                     it.displayName,
523                     it.authority,
524                     it.icon.getLoadableUri().toString(),
525                 )
526             )
527         }
528         return cursor
529     }
530 }
531