Skip to content

Commit

Permalink
Add progress indicator for downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
newmskywalker committed Jan 28, 2025
1 parent 91c739b commit 7ebfdb0
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 97 deletions.
1 change: 1 addition & 0 deletions android/app-newm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.material)
implementation(libs.androidx.media3.datasource)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.database)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.launchdarkly.client)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import androidx.activity.result.contract.ActivityResultContracts
import androidx.media3.database.DatabaseProvider
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
Expand All @@ -22,6 +23,8 @@ import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenPresenter
import io.newm.feature.login.screen.welcome.WelcomeScreenPresenter
import io.newm.feature.musicplayer.service.DownloadManager
import io.newm.feature.musicplayer.service.DownloadManagerImpl
import io.newm.feature.musicplayer.service.DownloadStateManager
import io.newm.feature.musicplayer.service.DownloadStateManagerImpl
import io.newm.screens.forceupdate.ForceAppUpdatePresenter
import io.newm.screens.library.NFTLibraryPresenter
import io.newm.screens.profile.edit.ProfileEditPresenter
Expand All @@ -33,6 +36,8 @@ import io.newm.utils.AndroidFeatureFlagManager
import io.newm.utils.ForceAppUpdateViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import java.util.concurrent.Executor
import androidx.media3.exoplayer.offline.DownloadManager as ExoDownloadManager

@SuppressLint("UnsafeOptInUsageError")
val viewModule = module {
Expand Down Expand Up @@ -127,7 +132,17 @@ val viewModule = module {
val downloadDirectory = androidContext().getExternalFilesDir(null)!!
SimpleCache(downloadDirectory, NoOpCacheEvictor(), get())
}
single<DownloadManager> { DownloadManagerImpl(androidContext()) }
single<DownloadManager> { DownloadManagerImpl(androidContext(), get()) }
single<DownloadStateManager> { DownloadStateManagerImpl(get(), get(), get()) }
single<ExoDownloadManager> {
ExoDownloadManager(
androidContext(),
get(),
get(),
DefaultHttpDataSource.Factory(),
Executor(Runnable::run)
)
}
}

val androidModules = module {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import io.newm.shared.public.usecases.HasWalletConnectionsUseCase
import io.newm.shared.public.usecases.SyncWalletConnectionsUseCase
import io.newm.shared.public.usecases.WalletNFTTracksUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

class NFTLibraryPresenter(
Expand Down Expand Up @@ -134,14 +136,23 @@ class NFTLibraryPresenter(
}
}

// Collect download states through DownloadManager
val downloadStates by remember(nftTracks) {
combine(
nftTracks.map { track ->
downloadManager.getDownloadState(track.id)
.map { state -> track.id to state }
}
) { states -> states.toMap() }
}.collectAsRetainedState(initial = emptyMap())

return when {
isLoading -> NFTLibraryState.Loading
isWalletConnected == false -> NFTLibraryState.LinkWallet { newmWalletConnectionId ->
scope.launch {
connectWalletUseCase.connect(newmWalletConnectionId)
}
}

isWalletEmpty -> NFTLibraryState.EmptyWallet
else -> {
NFTLibraryState.Content(
Expand All @@ -150,6 +161,7 @@ class NFTLibraryPresenter(
showZeroResultFound = showZeroResultFound,
filters = filters,
refreshing = refreshing,
downloadStates = downloadStates,
eventSink = { event ->
when (event) {
is NFTLibraryEvent.OnDownloadTrack -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
Expand Down Expand Up @@ -66,6 +69,7 @@ import io.newm.core.ui.text.SearchBar
import io.newm.core.ui.utils.ErrorScreen
import io.newm.core.ui.utils.drawWithBrush
import io.newm.core.ui.utils.textGradient
import io.newm.feature.musicplayer.service.DownloadState
import io.newm.screens.library.NFTLibraryEvent.OnApplyFilters
import io.newm.screens.library.NFTLibraryEvent.OnDownloadTrack
import io.newm.screens.library.NFTLibraryEvent.OnQueryChange
Expand Down Expand Up @@ -129,7 +133,8 @@ fun NFTLibraryScreenUi(
}
ErrorScreen(
title = stringResource(R.string.nft_library_error_message),
message =state.message)
message = state.message
)
}

is NFTLibraryState.Content -> {
Expand Down Expand Up @@ -164,6 +169,7 @@ fun NFTLibraryScreenUi(
onApplyFilters = { filters -> eventSink(OnApplyFilters(filters)) },
currentTrackId = state.currentTrackId,
downloadsEnabled = state.downloadsEnabled,
downloadStates = state.downloadStates,
)
}
}
Expand All @@ -188,6 +194,7 @@ private fun NFTTracks(
onApplyFilters: (NFTLibraryFilters) -> Unit,
currentTrackId: String?,
downloadsEnabled: Boolean,
downloadStates: Map<String, DownloadState>,
) {
val scope = rememberCoroutineScope()
val filterSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Expand Down Expand Up @@ -231,20 +238,17 @@ private fun NFTTracks(
showZeroResultsFound -> item { ZeroSearchResults() }

nftTracks.isNotEmpty() || streamTokenTracks.isNotEmpty() -> {
items(nftTracks + streamTokenTracks, key = { track ->
// Use the unique ID as the key
track.id
}) { track ->
items(nftTracks + streamTokenTracks, key = { track -> track.id }) { track ->
Box(
modifier = Modifier
.background(Gray16)
modifier = Modifier.background(Gray16)
) {
TrackRowItemWrapper(
track = track,
onPlaySong = onPlaySong,
onDownloadSong = { onDownloadSong(track) },
isSelected = track.id == currentTrackId,
downloadsEnabled = downloadsEnabled,
downloadState = downloadStates[track.id] ?: DownloadState.None,
)
}
}
Expand All @@ -270,6 +274,7 @@ private fun TrackRowItemWrapper(
onDownloadSong: () -> Unit,
isSelected: Boolean,
downloadsEnabled: Boolean,
downloadState: DownloadState,
) {
val swipeableState = rememberSwipeableState(initialValue = false)
val deltaX = with(LocalDensity.current) { 82.dp.toPx() }
Expand All @@ -289,8 +294,17 @@ private fun TrackRowItemWrapper(
),
)
) {
if (!track.isDownloaded && downloadsEnabled) {
RevealedPanel(onDownloadSong)
if (downloadsEnabled) {
val coroutineScope = rememberCoroutineScope()

RevealedPanel(
onDownloadClick = {
coroutineScope.launch {
swipeableState.animateTo(false)
}
onDownloadSong()
}
)
}
TrackRowItem(
track = track,
Expand All @@ -301,7 +315,8 @@ private fun TrackRowItemWrapper(
y = 0
)
} else Modifier,
isSelected = isSelected
isSelected = isSelected,
downloadState = downloadState,
)
}
}
Expand All @@ -311,15 +326,15 @@ private fun TrackRowItem(
track: NFTTrack,
onClick: (NFTTrack) -> Unit,
modifier: Modifier,
isSelected: Boolean
isSelected: Boolean,
downloadState: DownloadState,
) {
Row(
modifier = modifier
.background(color = Gray16)
.clickable(onClick = { onClick(track) })
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically

) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
Expand All @@ -345,14 +360,41 @@ private fun TrackRowItem(
color = if (isSelected) StatusGreen else White
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (track.isDownloaded) {
Icon(
painter = painterResource(id = R.drawable.ic_downloaded),
contentDescription = stringResource(R.string.downloaded_description),
tint = StatusGreen
)
Spacer(modifier = Modifier.size(4.dp))
when (downloadState) {
is DownloadState.Downloading -> {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = StatusGreen
)
Spacer(modifier = Modifier.size(4.dp))
}

is DownloadState.Failed -> {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Failed",
tint = MaterialTheme.colors.error,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.size(4.dp))
}

is DownloadState.Completed -> {
Icon(
painter = painterResource(id = R.drawable.ic_downloaded),
contentDescription = stringResource(R.string.downloaded_description),
tint = StatusGreen,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.size(4.dp))
}

DownloadState.None -> {
// Show nothing
}
}

Text(
text = track.artists.joinToString(","),
fontFamily = inter,
Expand All @@ -379,6 +421,11 @@ fun PreviewNftLibrary() {
showShortTracks = false
),
refreshing = false,
downloadStates = mapOf(
"track1" to DownloadState.Downloading(0.5f),
"track2" to DownloadState.Completed,
"track3" to DownloadState.Failed("Error message")
),
eventSink = {},
currentTrackId = null,
downloadsEnabled = true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.newm.screens.library

import com.slack.circuit.runtime.CircuitUiState
import io.newm.feature.musicplayer.service.DownloadState
import io.newm.shared.public.models.NFTTrack

sealed interface NFTLibraryState : CircuitUiState {
Expand All @@ -16,6 +17,7 @@ sealed interface NFTLibraryState : CircuitUiState {
val showZeroResultFound: Boolean,
val filters: NFTLibraryFilters,
val refreshing: Boolean,
val downloadStates: Map<String, DownloadState>,
val eventSink: (NFTLibraryEvent) -> Unit,
val currentTrackId: String?,
val downloadsEnabled: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ import android.net.Uri
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadRequest
import androidx.media3.exoplayer.offline.DownloadService
import kotlinx.coroutines.flow.Flow

interface DownloadManager {
fun download(id: String, url: String)
fun getDownloadState(id: String): Flow<DownloadState>
}

@UnstableApi
class DownloadManagerImpl(
private val context: Context
private val context: Context,
private val downloadStateManager: DownloadStateManager,
) : DownloadManager {

@UnstableApi
override fun download(id: String, url: String) {
downloadStateManager.updateDownloadState(id, DownloadState.Downloading(0f))

val uri = Uri.parse(url)
val downloadRequest = DownloadRequest.Builder(id, uri).build()

DownloadService.sendAddDownload(
context,
NewmDownloadService::class.java,
downloadRequest,
true // isForeground
true
)
}

override fun getDownloadState(id: String): Flow<DownloadState> {
return downloadStateManager.getDownloadState(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.newm.feature.musicplayer.service

sealed class DownloadState {
data object None : DownloadState()
data class Downloading(val progress: Float) : DownloadState()
data class Failed(val error: String) : DownloadState()
data object Completed : DownloadState()
}
Loading

0 comments on commit 7ebfdb0

Please sign in to comment.