From 42e98e62fa5d3324c1b8cb930660ecc92bacb5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 29 Jul 2024 12:38:49 +0200 Subject: [PATCH] feat: Add error handling and reload functionality to MediaStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added error handling and reload functionality to the MediaStore page. - Displayed a loading page while fetching songs from the MediaStore. - Showed an error page if an error occurred while fetching songs. - Added a reload button to the error page to allow users to retry fetching songs. Other changes: - Updated the date format for albums in the Spotify metadata details screen. - Removed unnecessary progress indicator from the metadata editor page. - Updated dependencies to include kotlinx.collections.immutable. - Added a safeDrawingPadding modifier to the LoadingPage composable. - Updated the PermissionNotGrantedDialog composable to use PersistentList for neededPermissions. Signed-off-by: Bobby Espinoza Signed-off-by: Gabriel Fontán --- .../com/bobbyesp/metadator/ext/ReleaseDate.kt | 23 ++++++++++ .../presentation/pages/MediaStorePage.kt | 16 ++++--- .../presentation/pages/MediaStoreViewModel.kt | 20 +++++---- .../presentation/pages/home/HomePage.kt | 6 ++- .../utilities/tageditor/MetadataEditorPage.kt | 33 +++++--------- .../utilities/tageditor/MetadataEditorVM.kt | 21 ++++++++- .../spotify/stages/SpMetadataBsDetails.kt | 11 +++-- .../bobbyesp/ui/common/pages/LoadingPage.kt | 45 ++++++++++++------- app/utilities/build.gradle.kts | 1 + .../permission/PermissionNotGrantedDialog.kt | 6 ++- .../ui/permission/PermissionRequestHandler.kt | 4 +- 11 files changed, 125 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/metadator/ext/ReleaseDate.kt diff --git a/app/src/main/java/com/bobbyesp/metadator/ext/ReleaseDate.kt b/app/src/main/java/com/bobbyesp/metadator/ext/ReleaseDate.kt new file mode 100644 index 0000000..bebe97f --- /dev/null +++ b/app/src/main/java/com/bobbyesp/metadator/ext/ReleaseDate.kt @@ -0,0 +1,23 @@ +package com.bobbyesp.metadator.ext + +import com.adamratzman.spotify.models.ReleaseDate + +fun ReleaseDate.format(precision: String?): String { + return when (precision) { + "year" -> { + "$year" + } + + "month" -> { + "$month - $year" + } + + "day" -> { + "$day-$month-$year" + } + + else -> { + "Unknown" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStorePage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStorePage.kt index 53cfe4d..7077cc4 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStorePage.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStorePage.kt @@ -12,16 +12,19 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.bobbyesp.metadator.R import com.bobbyesp.metadator.presentation.components.cards.songs.HorizontalSongCard import com.bobbyesp.metadator.presentation.components.cards.songs.VerticalSongCard import com.bobbyesp.metadator.presentation.components.others.status.EmptyMediaStore import com.bobbyesp.metadator.presentation.pages.home.LayoutType +import com.bobbyesp.ui.common.pages.ErrorPage +import com.bobbyesp.ui.common.pages.LoadingPage import com.bobbyesp.utilities.model.Song import com.bobbyesp.utilities.states.ResourceState import my.nanihadesuka.compose.LazyColumnScrollbar @@ -36,6 +39,7 @@ fun MediaStorePage( lazyGridState: LazyGridState, lazyListState: LazyListState, desiredLayout: LayoutType, + onReloadMediaStore: () -> Unit, onItemClicked: (Song) -> Unit ) { Box( @@ -45,13 +49,11 @@ fun MediaStorePage( targetState = desiredLayout, label = "List item transition", animationSpec = tween(200) ) { type -> when (songs.value) { - is ResourceState.Loading -> { - CircularProgressIndicator() - } - - is ResourceState.Error -> { + is ResourceState.Loading -> LoadingPage(text = stringResource(R.string.loading_mediastore)) - } + is ResourceState.Error -> ErrorPage( + error = songs.value.message ?: "Unknown" + ) { onReloadMediaStore() } is ResourceState.Success -> { if (songs.value.data!!.isEmpty()) { diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt index 1615e17..83a4b53 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt @@ -1,7 +1,6 @@ package com.bobbyesp.metadator.presentation.pages import android.content.Context -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.bobbyesp.utilities.mediastore.MediaStoreReceiver.Advanced.observeSongs @@ -28,31 +27,34 @@ class MediaStorePageViewModel @Inject constructor( private val mediaStoreSongsFlow = applicationContext.contentResolver.observeSongs() - fun reloadMediaStore() { + + private fun songsCollection() { viewModelScope.launch(Dispatchers.IO) { - _songs.update { ResourceState.Loading() } mediaStoreSongsFlow.collectLatest { songs -> _songs.update { ResourceState.Success(songs) } } } } + private fun reloadMediaStore() { + _songs.update { ResourceState.Loading() } + songsCollection() + } + fun onEvent(event: Events) { when (event) { is Events.StartObservingMediaStore -> { - Log.i("MediaStorePageViewModel", "Start observing media store") - viewModelScope.launch(Dispatchers.IO) { - mediaStoreSongsFlow.collectLatest { songs -> - _songs.update { ResourceState.Success(songs) } - } - } + songsCollection() } + + is Events.ReloadMediaStore -> reloadMediaStore() } } companion object { interface Events { data object StartObservingMediaStore : Events + data object ReloadMediaStore : Events } } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt index 62b7ed6..1819246 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt @@ -73,6 +73,7 @@ import com.bobbyesp.utilities.ui.rememberForeverLazyGridState import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @@ -225,7 +226,7 @@ fun HomePage( permissionState = storagePermissionState, deniedContent = { shouldShowRationale -> PermissionNotGrantedDialog( - neededPermissions = listOf(readAudioFiles.toPermissionType()), + neededPermissions = persistentListOf(readAudioFiles.toPermissionType()), onGrantRequest = { storagePermissionState.launchPermissionRequest() }, @@ -242,6 +243,9 @@ fun HomePage( lazyGridState = mediaStoreLazyGridState, lazyListState = mediaStoreLazyColumnState, desiredLayout = desiredLayout, + onReloadMediaStore = { + onEvent(MediaStorePageViewModel.Companion.Events.ReloadMediaStore) + }, onItemClicked = { song -> navController.navigate( Route.UtilitiesNavigator.TagEditor(song.toParcelableSong()) diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorPage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorPage.kt index 0ecb2a0..8399041 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorPage.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorPage.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetValue import androidx.compose.material3.Text @@ -64,6 +63,8 @@ import com.bobbyesp.metadator.presentation.common.LocalOrientation import com.bobbyesp.metadator.presentation.components.image.AsyncImage import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify.MetadataBsVM import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify.SpMetadataBottomSheetContent +import com.bobbyesp.ui.common.pages.ErrorPage +import com.bobbyesp.ui.common.pages.LoadingPage import com.bobbyesp.ui.components.button.CloseButton import com.bobbyesp.ui.components.others.MetadataTag import com.bobbyesp.ui.components.text.LargeCategoryTitle @@ -179,8 +180,16 @@ fun MetadataEditorPage( .navigationBarsPadding() ) { state -> when (state) { - is ScreenState.Error -> TODO() - ScreenState.Loading -> LoadingState(modifier = Modifier.fillMaxSize()) + is ScreenState.Error -> ErrorPage(error = state.exception.stackTrace.toString()) { + onEvent( + MetadataEditorVM.Event.LoadMetadata(receivedAudio.localPath) + ) + } + + ScreenState.Loading -> LoadingPage( + modifier = Modifier.fillMaxSize(), + text = stringResource(id = R.string.loading_metadata) + ) is ScreenState.Success -> { val scrollState = rememberScrollState() @@ -515,22 +524,4 @@ fun SongProperties(mutablePropertiesMap: SnapshotStateMap) { } } } -} - -@Composable -private fun LoadingState(modifier: Modifier = Modifier) { - Column( - modifier = modifier, verticalArrangement = Arrangement.spacedBy( - 8.dp, alignment = Alignment.CenterVertically - ), horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(id = R.string.loading_audio_information), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(0.7f) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorVM.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorVM.kt index 360e2ea..c4ab7ad 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorVM.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/MetadataEditorVM.kt @@ -9,6 +9,7 @@ import android.os.ParcelFileDescriptor import android.util.Log import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.bobbyesp.utilities.ext.toModifiableMap @@ -38,6 +39,7 @@ import javax.inject.Inject @HiltViewModel class MetadataEditorVM @Inject constructor( @ApplicationContext private val context: Context, + private val stateHandle: SavedStateHandle ) : ViewModel() { private val mutableState = MutableStateFlow(PageViewState()) val state = mutableState.asStateFlow() @@ -47,6 +49,12 @@ class MetadataEditorVM @Inject constructor( private var latestLoadedSongPath: String? = null + init { + stateHandle.get("path")?.let { + onEvent(Event.LoadMetadata(it)) + } + } + data class PageViewState( val metadata: ResourceState = ResourceState.Loading(), val audioProperties: ResourceState = ResourceState.Loading(), @@ -54,10 +62,22 @@ class MetadataEditorVM @Inject constructor( val mutablePropertiesMap: SnapshotStateMap = mutableStateMapOf() ) + override fun onCleared() { + super.onCleared() + updateState(ScreenState.Loading) + mutableState.update { + it.copy( + metadata = ResourceState.Loading(), + audioProperties = ResourceState.Loading() + ) + } + } + private suspend fun loadTrackMetadata(path: String) { updateState(ScreenState.Loading) mutableState.value.mutablePropertiesMap.clear() runCatching { + stateHandle["path"] = path MediaStoreReceiver.getFileDescriptorFromPath(context, path, mode = "r")?.use { songFd -> val metadata = loadAudioMetadata(songFd) @@ -371,7 +391,6 @@ class MetadataEditorVM @Inject constructor( data class RequestPermission(val intent: PendingIntent) : UiEvent data class SaveSuccess(val pictures: Boolean? = null, val properties: Boolean? = null) : UiEvent - data object SaveFailed : UiEvent } diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/spotify/stages/SpMetadataBsDetails.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/spotify/stages/SpMetadataBsDetails.kt index e88741a..3dc28f4 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/spotify/stages/SpMetadataBsDetails.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/utilities/tageditor/spotify/stages/SpMetadataBsDetails.kt @@ -1,5 +1,6 @@ package com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify.stages +import android.content.Context import androidx.activity.compose.BackHandler import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -28,6 +29,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -38,6 +40,7 @@ import com.adamratzman.spotify.models.Track import com.bobbyesp.metadator.R import com.bobbyesp.metadator.ext.TagLib.toImageVector import com.bobbyesp.metadator.ext.TagLib.toLocalizedName +import com.bobbyesp.metadator.ext.format import com.bobbyesp.metadator.ext.formatArtistsName import com.bobbyesp.metadator.presentation.components.image.AsyncImage import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify.MetadataBsVM @@ -99,7 +102,8 @@ fun SpMetadataBsDetails( } } pageViewState.value.selectedTrack?.let { track -> - val metadataMap = createMetadataMap(track) + val context = LocalContext.current + val metadataMap = createMetadataMap(context, track) TrackInfo( modifier = Modifier.padding(vertical = 6.dp, horizontal = 8.dp), @@ -154,7 +158,7 @@ fun SpMetadataBsDetails( } @Composable -fun createMetadataMap(track: Track) = rememberSaveable { +fun createMetadataMap(context: Context, track: Track) = rememberSaveable { mutableMapOf( "TITLE" to track.name, "ARTIST" to track.artists.formatArtistsName(), @@ -162,7 +166,8 @@ fun createMetadataMap(track: Track) = rememberSaveable { "ALBUMARTIST" to track.album.artists.formatArtistsName(), "TRACKNUMBER" to track.trackNumber.toString(), "DISCNUMBER" to track.discNumber.toString(), - "DATE" to track.album.releaseDate.toString(), + "DATE" to (track.album.releaseDate?.format(track.album.releaseDatePrecisionString) + ?: context.getString(R.string.unknown)), ) } diff --git a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt index 9e6eeb1..110b819 100644 --- a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt +++ b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt @@ -1,31 +1,44 @@ package com.bobbyesp.ui.common.pages import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable -fun LoadingPage() { - Column( - modifier = Modifier +fun LoadingPage( + modifier: Modifier = Modifier, + text: String +) { + Box( + modifier = modifier .fillMaxSize() - .padding(16.dp) - .systemBarsPadding(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + .safeDrawingPadding(), + contentAlignment = Alignment.Center ) { - CircularProgressIndicator( - modifier = Modifier - .size(48.dp) - .align(Alignment.CenterHorizontally) - ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(0.7f) + ) + } } } \ No newline at end of file diff --git a/app/utilities/build.gradle.kts b/app/utilities/build.gradle.kts index 994964b..1e6628e 100644 --- a/app/utilities/build.gradle.kts +++ b/app/utilities/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(libs.paging.compose) implementation(libs.paging.runtime) implementation(libs.coil) + implementation(libs.kotlinx.collections.immutable) implementation(libs.bundles.coroutines) //Accompanist libraries implementation(libs.bundles.accompanist) diff --git a/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionNotGrantedDialog.kt b/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionNotGrantedDialog.kt index fe95f0c..2623c7e 100644 --- a/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionNotGrantedDialog.kt +++ b/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionNotGrantedDialog.kt @@ -27,11 +27,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.bobbyesp.utilities.R +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf @Composable fun PermissionNotGrantedDialog( modifier: Modifier = Modifier, - neededPermissions: List, + neededPermissions: PersistentList, shouldShowRationale: Boolean = false, onGrantRequest: () -> Unit, onDismissRequest: () -> Unit, @@ -155,7 +157,7 @@ fun PermissionNotGrantedPreview() { PermissionNotGrantedDialog( onGrantRequest = {}, onDismissRequest = {}, - neededPermissions = listOf( + neededPermissions = persistentListOf( PermissionType.READ_EXTERNAL_STORAGE, PermissionType.WRITE_EXTERNAL_STORAGE ) ) diff --git a/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionRequestHandler.kt b/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionRequestHandler.kt index ce4e635..8b9fa75 100644 --- a/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionRequestHandler.kt +++ b/app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionRequestHandler.kt @@ -18,7 +18,9 @@ fun PermissionRequestHandler( } is PermissionStatus.Denied -> { - deniedContent((permissionState.status as PermissionStatus.Denied).shouldShowRationale) + deniedContent( + (permissionState.status as PermissionStatus.Denied).shouldShowRationale + ) } } }