From f7e71da8278fab623fabeea0f81f852d25a6bbf4 Mon Sep 17 00:00:00 2001 From: shabinder Date: Tue, 23 Feb 2021 03:12:58 +0530 Subject: [PATCH] MutableSharedFlow,Active Tracks Query ,etc fixes --- .../com/shabinder/android/MainActivity.kt | 20 +++++----- .../shabinder/common/list/SpotiFlyerList.kt | 8 +++- .../list/integration/SpotiFlyerListImpl.kt | 4 ++ .../common/list/store/SpotiFlyerListStore.kt | 1 + .../list/store/SpotiFlyerListStoreProvider.kt | 35 ++++++++++------- .../shabinder/common/main/SpotiFlyerMainUi.kt | 2 +- .../shabinder/common/root/SpotiFlyerRoot.kt | 3 +- .../root/integration/SpotiFlyerRootImpl.kt | 2 + .../com/shabinder/common/di/AndroidActual.kt | 7 ++++ .../common/di/worker/ForegroundService.kt | 39 +++++++++++-------- .../kotlin/com/shabinder/common/di/Expect.kt | 4 +- .../common/di/providers/YoutubeMusic.kt | 2 +- .../com/shabinder/common/di/DesktopActual.kt | 16 +++++--- 13 files changed, 92 insertions(+), 51 deletions(-) diff --git a/android/src/main/java/com/shabinder/android/MainActivity.kt b/android/src/main/java/com/shabinder/android/MainActivity.kt index 2b9925c8..50a4686a 100644 --- a/android/src/main/java/com/shabinder/android/MainActivity.kt +++ b/android/src/main/java/com/shabinder/android/MainActivity.kt @@ -33,8 +33,7 @@ import com.shabinder.common.ui.colorOffWhite import com.shabinder.database.Database import com.tonyodev.fetch2.Status import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import org.koin.android.ext.android.inject const val disableDozeCode = 1223 @@ -47,8 +46,7 @@ class MainActivity : ComponentActivity() { private lateinit var root: SpotiFlyerRoot private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks - //TODO pass updates from Foreground Service - private val downloadFlow = MutableStateFlow(hashMapOf()) + private val trackStatusFlow = MutableSharedFlow>(1) private lateinit var updateUIReceiver: BroadcastReceiver private lateinit var queryReceiver: BroadcastReceiver @@ -80,7 +78,7 @@ class MainActivity : ComponentActivity() { override val database = this@MainActivity.database override val fetchPlatformQueryResult = this@MainActivity.fetcher override val directories: Dir = this@MainActivity.dir - override val downloadProgressReport: StateFlow> = downloadFlow + override val downloadProgressReport: MutableSharedFlow> = trackStatusFlow } ) @@ -108,9 +106,9 @@ class MainActivity : ComponentActivity() { addAction(Status.QUEUED.name) addAction(Status.FAILED.name) addAction(Status.DOWNLOADING.name) + addAction(Status.COMPLETED.name) addAction("Progress") addAction("Converting") - addAction("track_download_completed") } updateUIReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -119,18 +117,20 @@ class MainActivity : ComponentActivity() { val trackDetails = intent.getParcelableExtra("track") trackDetails?.let { track -> lifecycleScope.launch { - val latestMap = downloadFlow.value.apply { + val latestMap = trackStatusFlow.replayCache.getOrElse(0 + ) { hashMapOf() }.apply { this[track.title] = when (intent.action) { Status.QUEUED.name -> DownloadStatus.Queued Status.FAILED.name -> DownloadStatus.Failed Status.DOWNLOADING.name -> DownloadStatus.Downloading() "Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0)) "Converting" -> DownloadStatus.Converting - "track_download_completed" -> DownloadStatus.Downloaded + Status.COMPLETED.name -> DownloadStatus.Downloaded else -> DownloadStatus.NotDownloaded } } - downloadFlow.emit(latestMap) + trackStatusFlow.emit(latestMap) + Log.i("Track Update",track.title + track.downloaded.toString()) } } } @@ -146,7 +146,7 @@ class MainActivity : ComponentActivity() { trackList?.let { list -> Log.i("Service Response", "${list.size} Tracks Active") lifecycleScope.launch { - downloadFlow.emit(list) + trackStatusFlow.emit(list) } } } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index 719ebe6f..c5242b14 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -14,6 +14,7 @@ import com.shabinder.common.utils.Consumer import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.TrackDetails import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow interface SpotiFlyerList { @@ -40,13 +41,18 @@ interface SpotiFlyerList { * */ suspend fun loadImage(url:String): ImageBitmap? + /* + * Sync Tracks Statuses + * */ + fun onRefreshTracksStatuses() + interface Dependencies { val storeFactory: StoreFactory val fetchQuery: FetchPlatformQueryResult val dir: Dir val link: String val listOutput: Consumer - val downloadProgressFlow: StateFlow> + val downloadProgressFlow: MutableSharedFlow> } sealed class Output { object Finished : Output() diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index 530bdb47..a82061de 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -42,5 +42,9 @@ internal class SpotiFlyerListImpl( listOutput.callback(SpotiFlyerList.Output.Finished) } + override fun onRefreshTracksStatuses() { + store.accept(Intent.RefreshTracksStatuses) + } + override suspend fun loadImage(url: String): ImageBitmap? = dir.loadImage(url) } \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt index 9d9dd7ce..c05b1689 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStore.kt @@ -10,5 +10,6 @@ internal interface SpotiFlyerListStore: Store { data class SearchLink(val link: String): Intent() data class StartDownload(val track:TrackDetails): Intent() data class StartDownloadAll(val trackList: List): Intent() + object RefreshTracksStatuses: Intent() } } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index 7147738d..aba31224 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -4,15 +4,18 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import com.arkivanov.mvikotlin.core.store.* import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor +import com.shabinder.common.database.getLogger import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.downloadTracks +import com.shabinder.common.di.queryActiveTracks import com.shabinder.common.list.SpotiFlyerList.State import com.shabinder.common.list.store.SpotiFlyerListStore.Intent import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.TrackDetails import com.shabinder.common.ui.showPopUpMessage +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -21,8 +24,9 @@ internal class SpotiFlyerListStoreProvider( private val storeFactory: StoreFactory, private val fetchQuery: FetchPlatformQueryResult, private val link: String, - private val downloadProgressFlow: StateFlow> + private val downloadProgressFlow: MutableSharedFlow> ) { + val logger = getLogger() fun provide(): SpotiFlyerListStore = object : SpotiFlyerListStore, Store by storeFactory.create( name = "SpotiFlyerListStore", @@ -42,8 +46,10 @@ internal class SpotiFlyerListStoreProvider( override suspend fun executeAction(action: Unit, getState: () -> State) { executeIntent(Intent.SearchLink(link),getState) + executeIntent(Intent.RefreshTracksStatuses,getState) downloadProgressFlow.collectLatest { map -> + logger.d(map.size.toString(),"ListStore: flow Updated") val updatedTrackList = getState().trackList.updateTracksStatuses(map) if(updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList)) } @@ -53,7 +59,7 @@ internal class SpotiFlyerListStoreProvider( when (intent) { is Intent.SearchLink -> fetchQuery.query(link)?.let{ result -> result.trackList = result.trackList.toMutableList() - dispatch((Result.ResultFetched(result,result.trackList.toMutableList().updateTracksStatuses(downloadProgressFlow.value)))) + dispatch((Result.ResultFetched(result,result.trackList.toMutableList().updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()})))) } is Intent.StartDownloadAll -> { @@ -68,12 +74,13 @@ internal class SpotiFlyerListStoreProvider( } it } - dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.value))) + dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()}))) } is Intent.StartDownload -> { downloadTracks(listOf(intent.track),fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata) dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued })) } + is Intent.RefreshTracksStatuses -> queryActiveTracks() } } } @@ -93,18 +100,18 @@ internal class SpotiFlyerListStoreProvider( return this } } -} - -private fun MutableList.updateTracksStatuses(map:HashMap):SnapshotStateList{ - val titleList = this.map { it.title } - val newStateList = mutableStateListOf() - for(newTrack in map){ - titleList.indexOf(newTrack.key).let { position -> - this.getOrNull(position)?.apply { downloaded = newTrack.value }?.also { updatedTrack -> - this[position] = updatedTrack + private fun MutableList.updateTracksStatuses(map:HashMap):SnapshotStateList{ + val titleList = this.map { it.title } + val newStateList = mutableStateListOf() + for(newTrack in map){ + titleList.indexOf(newTrack.key).let { position -> + this.getOrNull(position)?.apply { downloaded = newTrack.value }?.also { updatedTrack -> + this[position] = updatedTrack + logger.d(updatedTrack.toString(),"List Store Track Update") + } } } + newStateList.addAll(this) + return newStateList } - newStateList.addAll(this) - return newStateList } \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt index 843f227a..99237b32 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMainUi.kt @@ -53,7 +53,7 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain){ when(model.selectedCategory){ HomeCategory.About -> AboutColumn() HomeCategory.History -> HistoryColumn( - model.records, + model.records.sortedByDescending { it.id }, component::loadImage, component::onLinkSearch ) diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt index 1cfb2649..37942e4e 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -14,6 +14,7 @@ import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.database.Database import com.shabinder.common.root.integration.SpotiFlyerRootImpl import com.shabinder.common.utils.Consumer +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow interface SpotiFlyerRoot { @@ -32,7 +33,7 @@ interface SpotiFlyerRoot { val database: Database val fetchPlatformQueryResult: FetchPlatformQueryResult val directories: Dir - val downloadProgressReport: StateFlow> + val downloadProgressReport: MutableSharedFlow> } } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index 397675ba..13549cf2 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -1,5 +1,6 @@ package com.shabinder.common.root.integration +import co.touchlab.kermit.Kermit import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.RouterState import com.arkivanov.decompose.pop @@ -8,6 +9,7 @@ import com.arkivanov.decompose.router import com.arkivanov.decompose.statekeeper.Parcelable import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.value.Value +import com.shabinder.common.database.getLogger import com.shabinder.common.di.Dir import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.main.SpotiFlyerMain diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt index 601050c3..7331b49f 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt @@ -41,6 +41,13 @@ actual fun giveDonation(){ //TODO } +actual fun queryActiveTracks() { + val serviceIntent = Intent(appContext, ForegroundService::class.java).apply { + action = "query" + } + ContextCompat.startForegroundService(appContext, serviceIntent) +} + actual suspend fun downloadTracks( list: List, getYTIDBestMatch:suspend (String,TrackDetails)->String?, diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt index bb5add60..54154212 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt @@ -251,25 +251,30 @@ class ForegroundService : Service(),CoroutineScope{ } override fun onCompleted(download: Download) { - launch { - val track = requestMap[download.request] - removeFromNotification("Downloading ${track?.title}") - try{ - track?.let { - dir.saveFileWithMetadata(byteArrayOf(),it) - allTracksStatus[it.title] = DownloadStatus.Converting + val track = requestMap[download.request] + try{ + track?.let { + val job = launch { dir.saveFileWithMetadata(byteArrayOf(),it) } + allTracksStatus[it.title] = DownloadStatus.Converting + sendTrackBroadcast("Converting",it) + addToNotification("Processing ${it.title}") + job.invokeOnCompletion { _ -> + converted++ + sendTrackBroadcast(Status.COMPLETED.name,it) + removeFromNotification("Processing ${it.title}") } - logger.d(tag){"${track?.title} Download Completed"} - }catch ( - e: KotlinNullPointerException - ){ - logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"} - logger.d(tag){"${track?.title} Requesting Download thru Android DM"} - downloadUsingDM(download.request.url, download.request.file, track!!) - downloaded++ - requestMap.remove(download.request) } + logger.d(tag){"${track?.title} Download Completed"} + }catch ( + e: KotlinNullPointerException + ){ + logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"} + logger.d(tag){"${track?.title} Requesting Download thru Android DM"} + downloadUsingDM(download.request.url, download.request.file, track!!) } + downloaded++ + requestMap.remove(download.request) + removeFromNotification("Downloading ${track?.title}") } override fun onDeleted(download: Download) { @@ -427,7 +432,7 @@ class ForegroundService : Service(),CoroutineScope{ fetch.removeAll() updateNotification() cleanFiles(File(dir.defaultDir())) - cleanFiles(File(dir.imageCacheDir())) + //TODO cleanFiles(File(dir.imageCacheDir())) messageList = mutableListOf("","","","","") releaseWakeLock() serviceJob.cancel() diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt index 5697f0e4..82ff4093 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt @@ -12,4 +12,6 @@ expect suspend fun downloadTracks( list: List, getYTIDBestMatch:suspend (String,TrackDetails)->String?, saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit -) \ No newline at end of file +) + +expect fun queryActiveTracks() \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt index 3d7c6c0f..4cbeca1c 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt @@ -242,7 +242,7 @@ class YoutubeMusic constructor( val avgMatch = (artistMatch + durationMatch)/2 linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt() } - logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"} + //logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"} return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap() } diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index c429bbf8..cc68b897 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -22,7 +22,9 @@ actual fun giveDonation(){ //TODO } -val DownloadProgressFlow: MutableStateFlow> = MutableStateFlow(hashMapOf()) +actual fun queryActiveTracks(){} + +val DownloadProgressFlow: MutableSharedFlow> = MutableSharedFlow(1) actual suspend fun downloadTracks( list: List, @@ -36,7 +38,8 @@ actual suspend fun downloadTracks( val searchQuery = "${it.title} - ${it.artists.joinToString(",")}" val videoId = getYTIDBestMatch(searchQuery,it) if (videoId.isNullOrBlank()) { - DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(it.title,DownloadStatus.Failed) }) + DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0 + ) { hashMapOf() }.apply { set(it.title,DownloadStatus.Failed) }) } else {//Found Youtube Video ID downloadTrack(videoId, it,saveFileWithMetaData) } @@ -59,14 +62,17 @@ suspend fun downloadTrack( downloadFile(url).collect { when(it){ is DownloadResult.Error -> { - DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Failed) }) + DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0 + ) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Failed) }) } is DownloadResult.Progress -> { - DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Downloading(it.progress)) }) + DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0 + ) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Downloading(it.progress)) }) } is DownloadResult.Success -> {//Todo clear map saveFileWithMetaData(it.byteArray,trackDetails) - DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Downloaded) }) + DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0 + ) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Downloaded) }) } } }