diff options
13 files changed, 253 insertions, 49 deletions
diff --git a/photopicker/src/com/android/photopicker/data/DataService.kt b/photopicker/src/com/android/photopicker/data/DataService.kt index e91e8d142..737119011 100644 --- a/photopicker/src/com/android/photopicker/data/DataService.kt +++ b/photopicker/src/com/android/photopicker/data/DataService.kt @@ -40,7 +40,7 @@ interface DataService { val availableProviders: StateFlow<List<Provider>> /** @return an instance of [PagingSource]. */ - fun albumContentPagingSource(albumId: String): PagingSource<MediaPageKey, Media> + fun albumMediaPagingSource(albumId: String): PagingSource<MediaPageKey, Media> /** @return an instance of [PagingSource]. */ fun albumPagingSource(): PagingSource<MediaPageKey, Album> diff --git a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt index 1f5d85e4e..4b28aff90 100644 --- a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt +++ b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt @@ -203,7 +203,7 @@ class DataServiceImpl( mediaProviderClient.fetchAvailableProviders(resolver) } - override fun albumContentPagingSource(albumId: String): PagingSource<MediaPageKey, Media> = + override fun albumMediaPagingSource(albumId: String): PagingSource<MediaPageKey, Media> = throw NotImplementedError("This method is not implemented yet.") override fun albumPagingSource(): PagingSource<MediaPageKey, Album> = runBlocking { diff --git a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt index 135ffab78..a050154ce 100644 --- a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt +++ b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt @@ -144,7 +144,7 @@ open class MediaProviderClient { data = cursor.getListOfMedia(), prevKey = cursor.getPrevPageKey(), nextKey = cursor.getNextPageKey()) - } ?: throw IllegalStateException("Received a null response from Content Provider") + } ?: throw IllegalStateException("Received a null response from Content Provider") } } catch (e: RuntimeException) { throw RuntimeException("Could not fetch media", e) @@ -183,10 +183,50 @@ open class MediaProviderClient { data = cursor.getListOfAlbums(), prevKey = cursor.getPrevPageKey(), nextKey = cursor.getNextPageKey()) - } ?: throw IllegalStateException("Received a null response from Content Provider") + } ?: throw IllegalStateException("Received a null response from Content Provider") } } catch (e: RuntimeException) { - throw RuntimeException("Could not fetch media", e) + throw RuntimeException("Could not fetch albums", e) + } + } + + /** + * Fetch a list of [Media] from MediaProvider for the given page key. + */ + fun fetchAlbumMedia( + albumId: String, + pageKey: MediaPageKey, + pageSize: Int, + contentResolver: ContentResolver, + availableProviders: List<Provider>, + ): LoadResult<MediaPageKey, Media> { + val input: Bundle = bundleOf ( + MediaQuery.PICKER_ID.key to pageKey.pickerId, + MediaQuery.DATE_TAKEN.key to pageKey.dateTakenMillis, + MediaQuery.PAGE_SIZE.key to pageSize, + MediaQuery.PROVIDERS.key to ArrayList<String>().apply { + availableProviders.forEach { provider -> + add(provider.authority) + } + } + ) + + try { + return contentResolver.query( + getAlbumMediaUri(albumId), + /* projection */ null, + input, + /* cancellationSignal */ null // TODO + ).use { + cursor -> cursor?.let { + LoadResult.Page( + data = cursor.getListOfMedia(), + prevKey = cursor.getPrevPageKey(), + nextKey = cursor.getNextPageKey()) + } ?: throw IllegalStateException("Received a null response from Content Provider") + } + } catch (e: RuntimeException) { + throw RuntimeException("Could not fetch album media", e) } } diff --git a/photopicker/src/com/android/photopicker/data/UriHelper.kt b/photopicker/src/com/android/photopicker/data/UriHelper.kt index 457c280fb..0ee7e2445 100644 --- a/photopicker/src/com/android/photopicker/data/UriHelper.kt +++ b/photopicker/src/com/android/photopicker/data/UriHelper.kt @@ -72,3 +72,9 @@ val MEDIA_CHANGE_NOTIFICATION_URI: Uri = pickerUri.buildUpon().apply { val ALBUM_URI: Uri = pickerUri.buildUpon().apply { appendPath(ALBUM_PATH_SEGMENT) }.build() + +fun getAlbumMediaUri(albumId: String): Uri { + return ALBUM_URI.buildUpon().apply { + appendPath(albumId) + }.build() +} diff --git a/photopicker/src/com/android/photopicker/data/paging/AlbumContentPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt index 901278e7b..c1346342c 100644 --- a/photopicker/src/com/android/photopicker/data/paging/AlbumContentPagingSource.kt +++ b/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt @@ -16,11 +16,15 @@ package com.android.photopicker.data.paging -import android.content.Context +import android.content.ContentResolver +import android.util.Log import androidx.paging.PagingSource +import androidx.paging.PagingSource.LoadResult import androidx.paging.PagingState +import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaPageKey +import com.android.photopicker.data.model.Provider /** * This [PagingSource] class is responsible to providing paginated album media data from Picker @@ -28,16 +32,40 @@ import com.android.photopicker.data.model.MediaPageKey * * It sources data from a [ContentProvider] called [MediaProvider]. */ -class AlbumContentPagingSource( - context: Context, - albumId: String, +class AlbumMediaPagingSource( + private val albumId: String, + private val contentResolver: ContentResolver, + private val availableProviders: List<Provider>, + private val mediaProviderClient: MediaProviderClient, ) : PagingSource<MediaPageKey, Media>() { + companion object { + val TAG: String = "PickerAlbumMediaPagingSource" + } override suspend fun load( params: LoadParams<MediaPageKey> - ): LoadResult<MediaPageKey, Media> = - throw NotImplementedError("This method is not implemented yet.") + ): LoadResult<MediaPageKey, Media> { + val pageKey = params.key ?: MediaPageKey() + val pageSize = params.loadSize - override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = - throw NotImplementedError("This method is not implemented yet.") + return try { + + if (availableProviders.isEmpty()) { + throw IllegalArgumentException("No available providers found.") + } + + mediaProviderClient.fetchAlbumMedia( + albumId, + pageKey, + pageSize, + contentResolver, + availableProviders + ) + } catch (e: Exception) { + Log.e(TAG, "Could not fetch page from MediaProvider for album $albumId", e) + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = null }
\ No newline at end of file diff --git a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt index c0a78f1af..ffe6fab97 100644 --- a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt @@ -46,8 +46,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import src.com.android.photopicker.data.TestMediaProvider -import src.com.android.photopicker.data.TestNotificationServiceImpl @SmallTest @RunWith(AndroidJUnit4::class) diff --git a/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt b/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt index e2155a274..34de524f5 100644 --- a/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt +++ b/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt @@ -43,7 +43,7 @@ class TestDataServiceImpl() : DataService { override val availableProviders: StateFlow<List<Provider>> = MutableStateFlow(emptyList()) - override fun albumContentPagingSource(albumId: String): PagingSource<MediaPageKey, Media> = + override fun albumMediaPagingSource(albumId: String): PagingSource<MediaPageKey, Media> = throw NotImplementedError("This method is not implemented yet.") override fun albumPagingSource(): PagingSource<MediaPageKey, Album> = diff --git a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt index 54a9a08fe..99c693ed0 100644 --- a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt +++ b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package src.com.android.photopicker.data +package com.android.photopicker.data import android.database.Cursor import android.database.MatrixCursor @@ -22,7 +22,6 @@ import android.net.Uri import android.os.Bundle import android.os.CancellationSignal import android.test.mock.MockContentProvider -import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.model.Group import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaSource @@ -55,9 +54,15 @@ val DEFAULT_MEDIA: List<Media> = listOf( ) val DEFAULT_ALBUMS: List<Group.Album> = listOf( - createAlbum("Favorites"), - createAlbum("Downloads"), - createAlbum("CloudAlbum"), + createAlbum("Favorites"), + createAlbum("Downloads"), + createAlbum("CloudAlbum"), +) + +val DEFAULT_ALBUM_NAME = "album_id" + +val DEFAULT_ALBUM_MEDIA: Map<String, List<Media>> = mapOf( + DEFAULT_ALBUM_NAME to DEFAULT_MEDIA ) fun createMediaImage(pickerId: Long): Media { @@ -90,7 +95,8 @@ fun createAlbum(albumId: String): Group.Album { class TestMediaProvider( var providers: List<Provider> = DEFAULT_PROVIDERS, var media: List<Media> = DEFAULT_MEDIA, - var albums: List<Group.Album> = DEFAULT_ALBUMS + var albums: List<Group.Album> = DEFAULT_ALBUMS, + var albumMedia: Map<String, List<Media>> = DEFAULT_ALBUM_MEDIA ) : MockContentProvider() { var lastRefreshMediaRequest: Bundle? = null @@ -104,7 +110,15 @@ class TestMediaProvider( "available_providers" -> getAvailableProviders() "media" -> getMedia() "album" -> getAlbums() - else -> throw UnsupportedOperationException("Could not recognize uri $uri") + else -> { + val pathSegments: MutableList<String> = uri.getPathSegments() + if (pathSegments.size == 4 && pathSegments[2].equals("album")) { + // Album media query + return getAlbumMedia(pathSegments[3]) + } else { + throw UnsupportedOperationException("Could not recognize uri $uri") + } + } } } @@ -145,7 +159,7 @@ class TestMediaProvider( return cursor } - private fun getMedia(): Cursor { + private fun getMedia(mediaItems: List<Media> = media): Cursor { val cursor = MatrixCursor( arrayOf( MediaProviderClient.MediaResponse.MEDIA_ID.key, @@ -161,21 +175,21 @@ class TestMediaProvider( MediaProviderClient.MediaResponse.DURATION.key, ) ) - media.forEach { - media -> + mediaItems.forEach { + mediaItem -> cursor.addRow( arrayOf( - media.mediaId, - media.pickerId.toString(), - media.authority, - media.mediaSource.toString(), - media.mediaUri.toString(), - media.glideLoadableUri.toString(), - media.dateTakenMillisLong.toString(), - media.sizeInBytes.toString(), - media.mimeType, - media.standardMimeTypeExtension.toString(), - if (media is Media.Video) media.duration else "0" + mediaItem.mediaId, + mediaItem.pickerId.toString(), + mediaItem.authority, + mediaItem.mediaSource.toString(), + mediaItem.mediaUri.toString(), + mediaItem.glideLoadableUri.toString(), + mediaItem.dateTakenMillisLong.toString(), + mediaItem.sizeInBytes.toString(), + mediaItem.mimeType, + mediaItem.standardMimeTypeExtension.toString(), + if (mediaItem is Media.Video) mediaItem.duration else "0" ) ) } @@ -211,6 +225,11 @@ class TestMediaProvider( return cursor } + private fun getAlbumMedia(albumId: String): Cursor? { + return getMedia(albumMedia.getOrDefault(albumId, emptyList())) + } + + private fun initMedia(extras: Bundle?) { lastRefreshMediaRequest = extras } diff --git a/photopicker/tests/src/com/android/photopicker/data/TestNotificationServiceImpl.kt b/photopicker/tests/src/com/android/photopicker/data/TestNotificationServiceImpl.kt index 259a00e83..73c395561 100644 --- a/photopicker/tests/src/com/android/photopicker/data/TestNotificationServiceImpl.kt +++ b/photopicker/tests/src/com/android/photopicker/data/TestNotificationServiceImpl.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package src.com.android.photopicker.data +package com.android.photopicker.data import android.content.ContentResolver import android.content.UriMatcher import android.database.ContentObserver import android.net.Uri -import com.android.photopicker.data.NotificationService /** * Test implementation of Notification Service. It registers the observers in memory. Test writers diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt new file mode 100644 index 000000000..8bc3eb474 --- /dev/null +++ b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.photopicker.features.data.paging + +import android.content.ContentResolver +import androidx.paging.PagingSource.LoadParams +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.photopicker.data.MediaProviderClient +import com.android.photopicker.data.model.MediaPageKey +import com.android.photopicker.data.model.MediaSource +import com.android.photopicker.data.model.Provider +import com.android.photopicker.data.paging.AlbumMediaPagingSource +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import com.android.photopicker.data.TestMediaProvider + +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class AlbumMediaPagingSourceTest { + private val testContentProvider: TestMediaProvider = TestMediaProvider() + private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider) + private val availableProviders: List<Provider> = listOf(Provider("auth", MediaSource.LOCAL, 0)) + + @Mock + private lateinit var mockMediaProviderClient: MediaProviderClient + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testLoad() = runTest { + val albumId = "test-album-id" + val albumMediaPagingSource = AlbumMediaPagingSource( + albumId = albumId, + contentResolver = contentResolver, + availableProviders = availableProviders, + mediaProviderClient = mockMediaProviderClient + ) + + val pageKey = MediaPageKey() + val pageSize = 10 + val params = LoadParams.Append<MediaPageKey>( + key = pageKey, + loadSize = pageSize, + placeholdersEnabled = false + ) + + backgroundScope.launch { + albumMediaPagingSource.load(params) + } + advanceTimeBy(100) + + verify(mockMediaProviderClient, times(1)) + .fetchAlbumMedia( + albumId, + pageKey, + pageSize, + contentResolver, + availableProviders + ) + } +}
\ No newline at end of file diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt index 352e3dda2..6a72875c7 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt @@ -35,7 +35,7 @@ import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import src.com.android.photopicker.data.TestMediaProvider +import com.android.photopicker.data.TestMediaProvider @SmallTest @RunWith(AndroidJUnit4::class) diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt index c382c7ac4..b6fd7bb04 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt @@ -35,7 +35,7 @@ import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import src.com.android.photopicker.data.TestMediaProvider +import com.android.photopicker.data.TestMediaProvider @SmallTest @RunWith(AndroidJUnit4::class) diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt index 2882cb0a7..758fe85c0 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt @@ -30,8 +30,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith -import src.com.android.photopicker.data.TestMediaProvider - +import com.android.photopicker.data.TestMediaProvider @SmallTest @RunWith(AndroidJUnit4::class) @@ -136,12 +135,12 @@ class MediaProviderClientTest { val mediaProviderClient = MediaProviderClient() val albumLoadResult: LoadResult<MediaPageKey, Group.Album> = - mediaProviderClient.fetchAlbums( - pageKey = MediaPageKey(), - pageSize = 5, - contentResolver = testContentResolver, - availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0)) - ) + mediaProviderClient.fetchAlbums( + pageKey = MediaPageKey(), + pageSize = 5, + contentResolver = testContentResolver, + availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0)) + ) assertThat(albumLoadResult is LoadResult.Page).isTrue() @@ -152,4 +151,30 @@ class MediaProviderClientTest { assertThat(albums[index]).isEqualTo(testContentProvider.albums[index]) } } + + @Test + fun testFetchAlbumMediaPage() = runTest { + val mediaProviderClient = MediaProviderClient() + val albumId = testContentProvider.albumMedia.keys.elementAt(0) + + val mediaLoadResult: LoadResult<MediaPageKey, Media> = + mediaProviderClient.fetchAlbumMedia( + albumId = albumId, + pageKey = MediaPageKey(), + pageSize = 5, + contentResolver = testContentResolver, + availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0)) + ) + + assertThat(mediaLoadResult is LoadResult.Page).isTrue() + + val albumMedia: List<Media> = (mediaLoadResult as LoadResult.Page).data + + val expectedAlbumMedia = testContentProvider.albumMedia.get(albumId) + ?: emptyList() + assertThat(albumMedia.count()).isEqualTo(expectedAlbumMedia.count()) + for (index in albumMedia.indices) { + assertThat(albumMedia[index]).isEqualTo(expectedAlbumMedia[index]) + } + } }
\ No newline at end of file |