- Removed BroadcastReceivers and Bound to Service instead,

- Code Improv and Cleanup
This commit is contained in:
shabinder 2021-06-22 11:43:30 +05:30
parent 9b447c3a9d
commit 979fcc342b
11 changed files with 204 additions and 176 deletions

View File

@ -121,13 +121,14 @@ dependencies {
implementation(MVIKotlin.mvikotlinTimeTravel) implementation(MVIKotlin.mvikotlinTimeTravel)
// Extras // Extras
Extras.Android.apply { with(Extras.Android) {
implementation(Acra.notification) implementation(Acra.notification)
implementation(Acra.http) implementation(Acra.http)
implementation(appUpdator) implementation(appUpdator)
implementation(matomo) implementation(matomo)
} }
implementation(Extras.kermit)
//implementation("com.jakewharton.timber:timber:4.7.1") //implementation("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:0.7.0") implementation("dev.icerock.moko:parcelize:0.7.0")
implementation("com.github.shabinder:storage-chooser:2.0.4.45") implementation("com.github.shabinder:storage-chooser:2.0.4.45")

View File

@ -72,6 +72,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="com.shabinder.common.di.worker.ForegroundService"/> <service android:name=".service.ForegroundService"/>
</application> </application>
</manifest> </manifest>

View File

@ -17,15 +17,16 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.ServiceConnection
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@ -51,18 +52,17 @@ import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsHeight import com.google.accompanist.insets.statusBarsHeight
import com.google.accompanist.insets.statusBarsPadding import com.google.accompanist.insets.statusBarsPadding
import com.shabinder.common.di.* import com.shabinder.common.di.*
import com.shabinder.common.di.worker.ForegroundService
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods import com.shabinder.common.models.methods
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.SpotiFlyerRoot.Analytics
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.uikit.* import com.shabinder.common.uikit.*
import com.shabinder.spotiflyer.service.ForegroundService
import com.shabinder.spotiflyer.ui.AnalyticsDialog import com.shabinder.spotiflyer.ui.AnalyticsDialog
import com.shabinder.spotiflyer.ui.NetworkDialog import com.shabinder.spotiflyer.ui.NetworkDialog
import com.shabinder.spotiflyer.ui.PermissionDialog import com.shabinder.spotiflyer.ui.PermissionDialog
@ -82,12 +82,16 @@ class MainActivity : ComponentActivity() {
private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks
private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1) private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
private var permissionGranted = mutableStateOf(true) private var permissionGranted = mutableStateOf(true)
private lateinit var updateUIReceiver: BroadcastReceiver
private lateinit var queryReceiver: BroadcastReceiver
private val internetAvailability by lazy { ConnectionLiveData(applicationContext) } private val internetAvailability by lazy { ConnectionLiveData(applicationContext) }
private val tracker get() = (application as App).tracker private val tracker get() = (application as App).tracker
private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance
// Variable for storing instance of our service class
var foregroundService: ForegroundService? = null
// Boolean to check if our activity is bound to service or not
var isServiceBound: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// This app draws behind the system bars, so we want to handle fitting system windows // This app draws behind the system bars, so we want to handle fitting system windows
@ -162,8 +166,62 @@ class MainActivity : ComponentActivity() {
TrackHelper.track().download().with(tracker) TrackHelper.track().download().with(tracker)
} }
handleIntentFromExternalActivity() handleIntentFromExternalActivity()
initForegroundService()
} }
/*START: Foreground Service Handlers*/
private fun initForegroundService() {
// Start and then Bind to the Service
ContextCompat.startForegroundService(
this@MainActivity,
Intent(this, ForegroundService::class.java)
)
bindService()
}
/**
* Interface for getting the instance of binder from our service class
* So client can get instance of our service class and can directly communicate with it.
*/
private val serviceConnection = object : ServiceConnection {
val tag = "Service Connection"
override fun onServiceConnected(className: ComponentName, iBinder: IBinder) {
Log.d(tag, "connected to service.")
// We've bound to MyService, cast the IBinder and get MyBinder instance
val binder = iBinder as ForegroundService.DownloadServiceBinder
foregroundService = binder.service
isServiceBound = true
lifecycleScope.launch {
foregroundService?.trackStatusFlowMap?.statusFlow?.let {
trackStatusFlow.emitAll(it.conflate())
}
}
}
override fun onServiceDisconnected(arg0: ComponentName) {
Log.d(tag, "disconnected from service.")
isServiceBound = false
}
}
/*Used to bind to our service class*/
private fun bindService() {
Intent(this, ForegroundService::class.java).also { intent ->
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
}
/*Used to unbind from our service class*/
private fun unbindService() {
Intent(this, ForegroundService::class.java).also {
unbindService(serviceConnection)
}
}
/*END: Foreground Service Handlers*/
@Composable @Composable
private fun isInternetAvailableState(): State<Boolean?> { private fun isInternetAvailableState(): State<Boolean?> {
return internetAvailability.observeAsState() return internetAvailability.observeAsState()
@ -206,12 +264,9 @@ class MainActivity : ComponentActivity() {
) )
} }
override fun sendTracksToService(array: ArrayList<TrackDetails>) { override fun sendTracksToService(array: List<TrackDetails>) {
for (list in array.chunked(50)) { if (foregroundService == null) initForegroundService()
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java) foregroundService?.downloadAllTracks(array)
serviceIntent.putParcelableArrayListExtra("object", list as ArrayList)
ContextCompat.startForegroundService(this@MainActivity, serviceIntent)
}
} }
} }
@ -296,10 +351,16 @@ class MainActivity : ComponentActivity() {
) )
private fun queryActiveTracks() { private fun queryActiveTracks() {
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply { lifecycleScope.launch {
action = "query" foregroundService?.trackStatusFlowMap?.let { tracksStatus ->
trackStatusFlow.emit(tracksStatus)
}
} }
ContextCompat.startForegroundService(this@MainActivity, serviceIntent) }
override fun onResume() {
super.onResume()
queryActiveTracks()
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -357,80 +418,6 @@ class MainActivity : ComponentActivity() {
} }
} }
/*
* Broadcast Handlers
* */
private fun initializeBroadcast(){
val intentFilter = IntentFilter().apply {
addAction(Status.QUEUED.name)
addAction(Status.FAILED.name)
addAction(Status.DOWNLOADING.name)
addAction(Status.COMPLETED.name)
addAction("Progress")
addAction("Converting")
}
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//Update Flow with latest details
if (intent != null) {
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let { track ->
lifecycleScope.launch {
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
Status.COMPLETED.name -> DownloadStatus.Downloaded
else -> DownloadStatus.NotDownloaded
}
}
trackStatusFlow.emit(latestMap)
Log.i("Track Update",track.title + track.downloaded.toString())
}
}
}
}
}
val queryFilter = IntentFilter().apply { addAction("query_result") }
queryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
@Suppress("UNCHECKED_CAST")
val trackList = intent.getSerializableExtra("tracks") as? HashMap<String, DownloadStatus>?
trackList?.let { list ->
Log.i("Service Response", "${list.size} Tracks Active")
lifecycleScope.launch {
trackStatusFlow.emit(list)
}
}
}
}
}
registerReceiver(updateUIReceiver, intentFilter)
registerReceiver(queryReceiver, queryFilter)
}
override fun onResume() {
super.onResume()
initializeBroadcast()
if(visibleChild is SpotiFlyerRoot.Child.List) {
// Update Track List Statuses when Returning to App
queryActiveTracks()
}
}
override fun onPause() {
super.onPause()
unregisterReceiver(updateUIReceiver)
unregisterReceiver(queryReceiver)
}
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
handleIntentFromExternalActivity(intent) handleIntentFromExternalActivity(intent)
@ -455,6 +442,11 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onDestroy() {
super.onDestroy()
unbindService()
}
companion object { companion object {
const val disableDozeCode = 1223 const val disableDozeCode = 1223
} }

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.common.di.worker package com.shabinder.spotiflyer.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.DownloadManager import android.app.DownloadManager
@ -26,6 +26,7 @@ import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
@ -40,12 +41,13 @@ import com.shabinder.common.di.downloadFile
import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -68,7 +70,8 @@ class ForegroundService : Service(), CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO get() = serviceJob + Dispatchers.IO
private val allTracksStatus = hashMapOf<String, DownloadStatus>() val trackStatusFlowMap = TrackStatusFlowMap(MutableSharedFlow(replay = 1),this)
private var messageList = mutableListOf("", "", "", "", "") private var messageList = mutableListOf("", "", "", "", "")
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
@ -80,7 +83,16 @@ class ForegroundService : Service(), CoroutineScope {
private val logger: Kermit by inject() private val logger: Kermit by inject()
private val dir: Dir by inject() private val dir: Dir by inject()
override fun onBind(intent: Intent): IBinder? = null
inner class DownloadServiceBinder : Binder() {
// Return this instance of MyService so clients can call public methods
val service: ForegroundService
get() =// Return this instance of Foreground Service so clients can call public methods
this@ForegroundService
}
private val myBinder: IBinder = DownloadServiceBinder()
override fun onBind(intent: Intent): IBinder = myBinder
@SuppressLint("UnspecifiedImmutableFlag") @SuppressLint("UnspecifiedImmutableFlag")
override fun onCreate() { override fun onCreate() {
@ -110,31 +122,13 @@ class ForegroundService : Service(), CoroutineScope {
"query" -> { "query" -> {
val response = Intent().apply { val response = Intent().apply {
action = "query_result" action = "query_result"
synchronized(allTracksStatus) { synchronized(trackStatusFlowMap) {
putExtra("tracks", allTracksStatus) putExtra("tracks", trackStatusFlowMap)
} }
} }
sendBroadcast(response) sendBroadcast(response)
} }
} }
val downloadObjects: ArrayList<TrackDetails>? = (
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object"
)
)
downloadObjects?.let { list ->
downloadObjects.size.let { size ->
total += size
isSingleDownload = (size == 1)
}
list.forEach { track ->
allTracksStatus[track.title] = DownloadStatus.Queued
}
updateNotification()
downloadAllTracks(list)
}
} }
// Wake locks and misc tasks from here : // Wake locks and misc tasks from here :
return if (isServiceStarted) { return if (isServiceStarted) {
@ -156,8 +150,16 @@ class ForegroundService : Service(), CoroutineScope {
/** /**
* Function To Download All Tracks Available in a List * Function To Download All Tracks Available in a List
**/ **/
private fun downloadAllTracks(trackList: List<TrackDetails>) { fun downloadAllTracks(trackList: List<TrackDetails>) {
trackList.size.also { size ->
total += size
isSingleDownload = (size == 1)
updateNotification()
}
trackList.forEach { trackList.forEach {
trackStatusFlowMap[it.title] = DownloadStatus.Queued
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
downloadService.execute { downloadService.execute {
fetcher.findMp3DownloadLink(it).fold( fetcher.findMp3DownloadLink(it).fold(
@ -165,10 +167,9 @@ class ForegroundService : Service(), CoroutineScope {
enqueueDownload(url, it) enqueueDownload(url, it)
}, },
failure = { _ -> failure = { _ ->
sendTrackBroadcast(Status.FAILED.name, it)
failed++ failed++
updateNotification() updateNotification()
allTracksStatus[it.title] = DownloadStatus.Failed trackStatusFlowMap[it.title] = DownloadStatus.Failed
} }
) )
} }
@ -180,24 +181,20 @@ class ForegroundService : Service(), CoroutineScope {
// Initiating Download // Initiating Download
addToNotification("Downloading ${track.title}") addToNotification("Downloading ${track.title}")
logger.d(tag) { "${track.title} Download Started" } logger.d(tag) { "${track.title} Download Started" }
allTracksStatus[track.title] = DownloadStatus.Downloading() trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
sendTrackBroadcast(Status.DOWNLOADING.name, track)
// Enqueueing Download // Enqueueing Download
downloadFile(url).collect { downloadFile(url).collect {
when (it) { when (it) {
is DownloadResult.Error -> { is DownloadResult.Error -> {
launch { logger.d(tag) { it.message }
logger.d(tag) { it.message } removeFromNotification("Downloading ${track.title}")
removeFromNotification("Downloading ${track.title}") failed++
failed++ updateNotification()
updateNotification()
sendTrackBroadcast(Status.FAILED.name, track)
}
} }
is DownloadResult.Progress -> { is DownloadResult.Progress -> {
allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress) trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress)
logger.d(tag) { "${track.title} Progress: ${it.progress} %" } logger.d(tag) { "${track.title} Progress: ${it.progress} %" }
val intent = Intent().apply { val intent = Intent().apply {
@ -209,26 +206,31 @@ class ForegroundService : Service(), CoroutineScope {
} }
is DownloadResult.Success -> { is DownloadResult.Success -> {
try { coroutineScope {
// Save File and Embed Metadata try {
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} } // Save File and Embed Metadata
allTracksStatus[track.title] = DownloadStatus.Converting val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
sendTrackBroadcast("Converting", track)
addToNotification("Processing ${track.title}") // Send Converting Status
job.invokeOnCompletion { trackStatusFlowMap[track.title] = DownloadStatus.Converting
converted++ addToNotification("Processing ${track.title}")
allTracksStatus[track.title] = DownloadStatus.Downloaded
sendTrackBroadcast(Status.COMPLETED.name, track) // All Processing Completed for this Track
removeFromNotification("Processing ${track.title}") job.invokeOnCompletion {
converted++
trackStatusFlowMap[track.title] = DownloadStatus.Downloaded
removeFromNotification("Processing ${track.title}")
}
logger.d(tag) { "${track.title} Download Completed" }
downloaded++
} catch (e: Exception) {
e.printStackTrace()
// Download Failed
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
failed++
} }
logger.d(tag) { "${track.title} Download Completed" } removeFromNotification("Downloading ${track.title}")
downloaded++
} catch (e: Exception) {
// Download Failed
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
failed++
} }
removeFromNotification("Downloading ${track.title}")
} }
} }
} }
@ -270,7 +272,7 @@ class ForegroundService : Service(), CoroutineScope {
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
downloadService.close() downloadService.close()
updateNotification() updateNotification()
cleanFiles(File(dir.defaultDir()), logger) cleanFiles(File(dir.defaultDir()))
// TODO cleanFiles(File(dir.imageCacheDir())) // TODO cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("", "", "", "", "") messageList = mutableListOf("", "", "", "", "")
releaseWakeLock() releaseWakeLock()
@ -336,12 +338,4 @@ class ForegroundService : Service(), CoroutineScope {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(notificationId, getNotification()) mNotificationManager.notify(notificationId, getNotification())
} }
private fun sendTrackBroadcast(action: String, track: TrackDetails) {
val intent = Intent().apply {
setAction(action)
putExtra("track", track)
}
this@ForegroundService.sendBroadcast(intent)
}
} }

View File

@ -0,0 +1,17 @@
package com.shabinder.spotiflyer.service
import com.shabinder.common.models.DownloadStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
class TrackStatusFlowMap(
val statusFlow: MutableSharedFlow<HashMap<String,DownloadStatus>>,
private val scope: CoroutineScope
): HashMap<String,DownloadStatus>() {
override fun put(key: String, value: DownloadStatus): DownloadStatus? {
val res = super.put(key, value)
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }
return res
}
}

View File

@ -1,22 +1,22 @@
package com.shabinder.common.di.worker package com.shabinder.spotiflyer.service
import co.touchlab.kermit.Kermit import android.util.Log
import java.io.File import java.io.File
/** /**
* Cleaning All Residual Files except Mp3 Files * Cleaning All Residual Files except Mp3 Files
**/ **/
fun cleanFiles(dir: File, logger: Kermit) { fun cleanFiles(dir: File) {
try { try {
logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " } Log.d("File Cleaning","Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles() val fList = dir.listFiles()
fList?.let { fList?.let {
for (file in fList) { for (file in fList) {
if (file.isDirectory) { if (file.isDirectory) {
cleanFiles(file, logger) cleanFiles(file)
} else if (file.isFile) { } else if (file.isFile) {
if (file.path.toString().substringAfterLast(".") != "mp3") { if (file.path.toString().substringAfterLast(".") != "mp3") {
logger.d("Files Cleaning") { "Cleaning ${file.path}" } Log.d("Files Cleaning","Cleaning ${file.path}")
file.delete() file.delete()
} }
} }
@ -24,3 +24,4 @@ fun cleanFiles(dir: File, logger: Kermit) {
} }
} catch (e: Exception) { e.printStackTrace() } } catch (e: Exception) { e.printStackTrace() }
} }

View File

@ -17,12 +17,32 @@
package com.shabinder.common.uikit package com.shabinder.common.uikit
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.* import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.* import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -53,6 +73,7 @@ fun SpotiFlyerListContent(
component.onBackPressed() component.onBackPressed()
} }
} }
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
val result = model.queryResult val result = model.queryResult
if (result == null) { if (result == null) {

View File

@ -14,7 +14,7 @@ actual interface PlatformActions {
fun addToLibrary(path: String) fun addToLibrary(path: String)
fun sendTracksToService(array: ArrayList<TrackDetails>) fun sendTracksToService(array: List<TrackDetails>)
} }
actual val StubPlatformActions = object : PlatformActions { actual val StubPlatformActions = object : PlatformActions {
@ -24,5 +24,5 @@ actual val StubPlatformActions = object : PlatformActions {
override fun addToLibrary(path: String) {} override fun addToLibrary(path: String) {}
override fun sendTracksToService(array: ArrayList<TrackDetails>) {} override fun sendTracksToService(array: List<TrackDetails>) {}
} }

View File

@ -23,11 +23,9 @@ import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database import com.shabinder.database.Database
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.*
import io.ktor.client.request.get import io.ktor.client.statement.*
import io.ktor.client.statement.HttpStatement import io.ktor.http.*
import io.ktor.http.contentLength
import io.ktor.http.isSuccess
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -105,7 +103,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
var offset = 0 var offset = 0
do { do {
// Set Length optimally, after how many kb you want a progress update, now it 0.25mb // Set Length optimally, after how many kb you want a progress update, now it 0.25mb
val currentRead = response.content.readAvailable(data, offset, 250000) val currentRead = response.content.readAvailable(data, offset, 2_50_000)
offset += currentRead offset += currentRead
val progress = (offset * 100f / data.size).roundToInt() val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress)) emit(DownloadResult.Progress(progress))

View File

@ -18,6 +18,7 @@ package com.shabinder.common.list.integration
import co.touchlab.stately.ensureNeverFrozen import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.lifecycle.doOnResume
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.shabinder.common.caching.Cache import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture import com.shabinder.common.di.Picture
@ -38,6 +39,9 @@ internal class SpotiFlyerListImpl(
init { init {
instanceKeeper.ensureNeverFrozen() instanceKeeper.ensureNeverFrozen()
lifecycle.doOnResume {
onRefreshTracksStatuses()
}
} }
private val store = private val store =

View File

@ -60,7 +60,7 @@ internal class SpotiFlyerListStoreProvider(
data class UpdateTrackList(val list: List<TrackDetails>) : Result() data class UpdateTrackList(val list: List<TrackDetails>) : Result()
data class UpdateTrackItem(val item: TrackDetails) : Result() data class UpdateTrackItem(val item: TrackDetails) : Result()
data class ErrorOccurred(val error: Throwable) : Result() data class ErrorOccurred(val error: Throwable) : Result()
data class AskForDonation(val isAllowed: Boolean) : Result() data class AskForSupport(val isAllowed: Boolean) : Result()
} }
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() { private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
@ -73,7 +73,7 @@ internal class SpotiFlyerListStoreProvider(
logger.d(message = "Database List Last ID: $it", tag = "Database Last ID") logger.d(message = "Database List Last ID: $it", tag = "Database Last ID")
val offset = dir.getDonationOffset val offset = dir.getDonationOffset
dispatch( dispatch(
Result.AskForDonation( Result.AskForSupport(
// Every 3rd Interval or After some offset // Every 3rd Interval or After some offset
isAllowed = offset < 4 && (it % offset == 0L) isAllowed = offset < 4 && (it % offset == 0L)
) )
@ -81,7 +81,7 @@ internal class SpotiFlyerListStoreProvider(
} }
downloadProgressFlow.collect { map -> downloadProgressFlow.collect { map ->
logger.d(map.size.toString(), "ListStore: flow Updated") // logger.d(map.size.toString(), "ListStore: flow Updated")
val updatedTrackList = getState().trackList.updateTracksStatuses(map) val updatedTrackList = getState().trackList.updateTracksStatuses(map)
if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList)) if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
} }
@ -131,7 +131,7 @@ internal class SpotiFlyerListStoreProvider(
is Result.UpdateTrackList -> copy(trackList = result.list) is Result.UpdateTrackList -> copy(trackList = result.list)
is Result.UpdateTrackItem -> updateTrackItem(result.item) is Result.UpdateTrackItem -> updateTrackItem(result.item)
is Result.ErrorOccurred -> copy(errorOccurred = result.error) is Result.ErrorOccurred -> copy(errorOccurred = result.error)
is Result.AskForDonation -> copy(askForDonation = result.isAllowed) is Result.AskForSupport -> copy(askForDonation = result.isAllowed)
} }
private fun State.updateTrackItem(item: TrackDetails): State { private fun State.updateTrackItem(item: TrackDetails): State {