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 com.android.photopicker.data.model.Group
26 import com.android.photopicker.data.model.Media
27 import com.android.photopicker.data.model.MediaSource
28 import com.android.photopicker.data.model.Provider
29 import java.util.UUID
30 
31 /**
32  * A test utility that provides implementation for some MediaProvider queries.
33  *
34  * This will be used to wrap [ContentResolver] to intercept calls to it and re-route them to the
35  * internal mock this class holds.
36  *
37  * All not overridden / unimplemented operations will throw [UnsupportedOperationException].
38  */
39 
40 val DEFAULT_PROVIDERS: List<Provider> = listOf(
41     Provider(
42         authority = "test_authority",
43         mediaSource = MediaSource.LOCAL,
44         uid = 0
45     )
46 )
47 
48 val DEFAULT_MEDIA: List<Media> = listOf(
49     createMediaImage(10),
50     createMediaImage(11),
51     createMediaImage(12),
52     createMediaImage(13),
53     createMediaImage(14),
54 )
55 
56 val DEFAULT_ALBUMS: List<Group.Album> = listOf(
57     createAlbum("Favorites"),
58     createAlbum("Downloads"),
59     createAlbum("CloudAlbum"),
60 )
61 
62 val DEFAULT_ALBUM_NAME = "album_id"
63 
64 val DEFAULT_ALBUM_MEDIA: Map<String, List<Media>> = mapOf(
65     DEFAULT_ALBUM_NAME to DEFAULT_MEDIA
66 )
67 
68 fun createMediaImage(pickerId: Long): Media {
69     return Media.Image(
70         mediaId = UUID.randomUUID().toString(),
71         pickerId = pickerId,
72         authority = "authority",
73         mediaSource = MediaSource.LOCAL,
74         mediaUri = Uri.parse("content://media/picker/authority/media/$pickerId"),
75         glideLoadableUri = Uri.parse("content://authority/media/$pickerId"),
76         dateTakenMillisLong = Long.MAX_VALUE,
77         sizeInBytes = 10,
78         mimeType = "image/*",
79         standardMimeTypeExtension = 0
80     )
81 }
82 
createAlbumnull83 fun createAlbum(albumId: String): Group.Album {
84     return Group.Album(
85         id = albumId,
86         pickerId = albumId.hashCode().toLong(),
87         authority = "authority",
88         dateTakenMillisLong = Long.MAX_VALUE,
89         displayName = albumId,
90         coverUri = Uri.parse("content://media/picker/authority/media/$albumId"),
91         coverMediaSource = MediaSource.LOCAL
92     )
93 }
94 
95 class TestMediaProvider(
96     var providers: List<Provider> = DEFAULT_PROVIDERS,
97     var media: List<Media> = DEFAULT_MEDIA,
98     var albums: List<Group.Album> = DEFAULT_ALBUMS,
99     var albumMedia: Map<String, List<Media>> = DEFAULT_ALBUM_MEDIA
100 ) : MockContentProvider() {
101     var lastRefreshMediaRequest: Bundle? = null
102 
querynull103     override fun query (
104         uri: Uri,
105         projection: Array<String>?,
106         queryArgs: Bundle?,
107         cancellationSignal: CancellationSignal?
108     ): Cursor? {
109         return when (uri.lastPathSegment) {
110             "available_providers" -> getAvailableProviders()
111             "media" -> getMedia()
112             "album" -> getAlbums()
113             else -> {
114                 val pathSegments: MutableList<String> = uri.getPathSegments()
115                 if (pathSegments.size == 4 && pathSegments[2].equals("album")) {
116                     // Album media query
117                     return getAlbumMedia(pathSegments[3])
118                 } else {
119                     throw UnsupportedOperationException("Could not recognize uri $uri")
120                 }
121             }
122         }
123     }
124 
callnull125     override fun call (
126             authority: String,
127             method: String,
128             arg: String?,
129             extras: Bundle?
130     ): Bundle? {
131         return when (method) {
132             "picker_media_init" -> {
133                 initMedia(extras)
134                 null
135             }
136             else -> throw UnsupportedOperationException("Could not recognize method $method")
137         }
138     }
139 
140     /**
141      * Returns a [Cursor] with the providers currently in the [providers] list.
142      */
getAvailableProvidersnull143     private fun getAvailableProviders(): Cursor {
144         val cursor = MatrixCursor(arrayOf(
145             MediaProviderClient.AvailableProviderResponse.AUTHORITY.key,
146             MediaProviderClient.AvailableProviderResponse.MEDIA_SOURCE.key,
147             MediaProviderClient.AvailableProviderResponse.UID.key
148         ))
149         providers.forEach {
150             provider ->
151                 cursor.addRow(
152                     arrayOf(
153                         provider.authority,
154                         provider.mediaSource.name,
155                         provider.uid.toString()
156                     )
157                 )
158         }
159         return cursor
160     }
161 
getMedianull162     private fun getMedia(mediaItems: List<Media> = media): Cursor {
163         val cursor = MatrixCursor(
164             arrayOf(
165                 MediaProviderClient.MediaResponse.MEDIA_ID.key,
166                 MediaProviderClient.MediaResponse.PICKER_ID.key,
167                 MediaProviderClient.MediaResponse.AUTHORITY.key,
168                 MediaProviderClient.MediaResponse.MEDIA_SOURCE.key,
169                 MediaProviderClient.MediaResponse.MEDIA_URI.key,
170                 MediaProviderClient.MediaResponse.LOADABLE_URI.key,
171                 MediaProviderClient.MediaResponse.DATE_TAKEN.key,
172                 MediaProviderClient.MediaResponse.SIZE.key,
173                 MediaProviderClient.MediaResponse.MIME_TYPE.key,
174                 MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key,
175                 MediaProviderClient.MediaResponse.DURATION.key,
176             )
177         )
178         mediaItems.forEach {
179             mediaItem ->
180                 cursor.addRow(
181                     arrayOf(
182                         mediaItem.mediaId,
183                         mediaItem.pickerId.toString(),
184                         mediaItem.authority,
185                         mediaItem.mediaSource.toString(),
186                         mediaItem.mediaUri.toString(),
187                         mediaItem.glideLoadableUri.toString(),
188                         mediaItem.dateTakenMillisLong.toString(),
189                         mediaItem.sizeInBytes.toString(),
190                         mediaItem.mimeType,
191                         mediaItem.standardMimeTypeExtension.toString(),
192                         if (mediaItem is Media.Video) mediaItem.duration else "0"
193                     )
194                 )
195         }
196         return cursor
197     }
198 
getAlbumsnull199     private fun getAlbums(): Cursor {
200         val cursor = MatrixCursor(
201             arrayOf(
202                 MediaProviderClient.AlbumResponse.ALBUM_ID.key,
203                 MediaProviderClient.AlbumResponse.PICKER_ID.key,
204                 MediaProviderClient.AlbumResponse.AUTHORITY.key,
205                 MediaProviderClient.AlbumResponse.DATE_TAKEN.key,
206                 MediaProviderClient.AlbumResponse.ALBUM_NAME.key,
207                 MediaProviderClient.AlbumResponse.UNWRAPPED_COVER_URI.key,
208                 MediaProviderClient.AlbumResponse.COVER_MEDIA_SOURCE.key,
209             )
210         )
211         albums.forEach {
212             album ->
213                 cursor.addRow(
214                     arrayOf(
215                         album.id,
216                         album.pickerId.toString(),
217                         album.authority,
218                         album.dateTakenMillisLong.toString(),
219                         album.displayName,
220                         album.coverUri.toString(),
221                         album.coverMediaSource.toString(),
222                     )
223                 )
224         }
225         return cursor
226     }
227 
getAlbumMedianull228     private fun getAlbumMedia(albumId: String): Cursor? {
229         return getMedia(albumMedia.getOrDefault(albumId, emptyList()))
230     }
231 
232 
initMedianull233     private fun initMedia(extras: Bundle?) {
234         lastRefreshMediaRequest = extras
235     }
236 }