Performance, Background Crashes and Notification Cancellation Fixes

This commit is contained in:
shabinder 2021-09-26 00:44:33 +05:30
parent 758fe62254
commit 8e32d4469b
12 changed files with 165 additions and 89 deletions

View File

@ -126,6 +126,7 @@ dependencies {
with(Versions.androidxLifecycle) { with(Versions.androidxLifecycle) {
implementation("androidx.lifecycle:lifecycle-service:$this") implementation("androidx.lifecycle:lifecycle-service:$this")
implementation("androidx.lifecycle:lifecycle-common-java8:$this") implementation("androidx.lifecycle:lifecycle-common-java8:$this")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$this")
} }
implementation(Extras.kermit) implementation(Extras.kermit)

View File

@ -40,7 +40,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.defaultComponentContext import com.arkivanov.decompose.defaultComponentContext
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
@ -71,10 +73,12 @@ 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
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -204,8 +208,10 @@ class MainActivity : ComponentActivity() {
foregroundService = binder.service foregroundService = binder.service
isServiceBound = true isServiceBound = true
lifecycleScope.launch { lifecycleScope.launch {
foregroundService?.trackStatusFlowMap?.statusFlow?.let { repeatOnLifecycle(Lifecycle.State.STARTED) {
trackStatusFlow.emitAll(it.conflate()) foregroundService?.trackStatusFlowMap?.statusFlow?.let {
trackStatusFlow.emitAll(it.conflate().flowOn(Dispatchers.Default))
}
} }
} }
} }

View File

@ -21,7 +21,6 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
@ -44,8 +43,6 @@ import com.shabinder.common.models.event.coroutines.failure
import com.shabinder.common.providers.FetchPlatformQueryResult import com.shabinder.common.providers.FetchPlatformQueryResult
import com.shabinder.common.translations.Strings import com.shabinder.common.translations.Strings
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.utils.autoclear.autoClear
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -57,12 +54,11 @@ import java.io.File
class ForegroundService : LifecycleService() { class ForegroundService : LifecycleService() {
private lateinit var downloadService: ParallelExecutor private lateinit var downloadService: ParallelExecutor
val trackStatusFlowMap by autoClear { val trackStatusFlowMap = TrackStatusFlowMap(
TrackStatusFlowMap( MutableSharedFlow(replay = 1),
MutableSharedFlow(replay = 1), lifecycleScope
lifecycleScope )
)
}
private val fetcher: FetchPlatformQueryResult by inject() private val fetcher: FetchPlatformQueryResult by inject()
private val logger: Kermit by inject() private val logger: Kermit by inject()
private val dir: FileManager by inject() private val dir: FileManager by inject()
@ -73,7 +69,12 @@ class ForegroundService : LifecycleService() {
private var isServiceStarted = false private var isServiceStarted = false
private val cancelIntent: PendingIntent by lazy { private val cancelIntent: PendingIntent by lazy {
val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" } val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" }
PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT) val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
PendingIntent.getService(this, 0, intent, flags)
} }
/* Variables Holding Download State */ /* Variables Holding Download State */
@ -98,6 +99,7 @@ class ForegroundService : LifecycleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
downloadService = ParallelExecutor(Dispatchers.IO) downloadService = ParallelExecutor(Dispatchers.IO)
trackStatusFlowMap.scope = lifecycleScope
createNotificationChannel(CHANNEL_ID, "Downloader Service") createNotificationChannel(CHANNEL_ID, "Downloader Service")
} }
@ -271,12 +273,16 @@ class ForegroundService : LifecycleService() {
private fun killService() { private fun killService() {
lifecycleScope.launch { lifecycleScope.launch {
logger.d(TAG) { "Killing Self" } logger.d(TAG) { "Killing Self" }
resetVar()
messageList = messageList.getEmpty().apply { messageList = messageList.getEmpty().apply {
set(index = 0, Message(Strings.cleaningAndExiting(), DownloadStatus.NotDownloaded)) set(index = 0, Message(Strings.cleaningAndExiting(), DownloadStatus.NotDownloaded))
} }
downloadService.close() downloadService.close()
updateNotification() updateNotification()
trackStatusFlowMap.clear() trackStatusFlowMap.apply {
clear()
scope = null
}
cleanFiles(File(dir.defaultDir())) cleanFiles(File(dir.defaultDir()))
// cleanFiles(File(dir.imageCacheDir())) // cleanFiles(File(dir.imageCacheDir()))
messageList = messageList.getEmpty() messageList = messageList.getEmpty()
@ -290,6 +296,13 @@ class ForegroundService : LifecycleService() {
} }
} }
private fun resetVar() {
total = 0
downloaded = 0
failed = 0
converted = 0
}
private fun createNotification(): Notification = private fun createNotification(): Notification =
NotificationCompat.Builder(this, CHANNEL_ID).run { NotificationCompat.Builder(this, CHANNEL_ID).run {
setSmallIcon(R.drawable.ic_download_arrow) setSmallIcon(R.drawable.ic_download_arrow)
@ -323,6 +336,7 @@ class ForegroundService : LifecycleService() {
updateNotification() updateNotification()
} }
@Suppress("unused")
private fun updateProgressInNotification(message: Message) { private fun updateProgressInNotification(message: Message) {
synchronized(messageList) { synchronized(messageList) {
val index = messageList.indexOfFirst { it.title == message.title } val index = messageList.indexOfFirst { it.title == message.title }
@ -331,10 +345,16 @@ class ForegroundService : LifecycleService() {
updateNotification() updateNotification()
} }
// Update Notification only if Service is Still Active
private fun updateNotification() { private fun updateNotification() {
val mNotificationManager: NotificationManager = if (!downloadService.isClosed.value) {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val mNotificationManager: NotificationManager =
mNotificationManager.notify(NOTIFICATION_ID, createNotification()) getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(NOTIFICATION_ID, createNotification())
} else {
// Service is Inactive so clear status
resetVar()
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@ -7,12 +7,12 @@ import kotlinx.coroutines.launch
class TrackStatusFlowMap( class TrackStatusFlowMap(
val statusFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>, val statusFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>,
private val scope: CoroutineScope var scope: CoroutineScope?
) : HashMap<String, DownloadStatus>() { ) : HashMap<String, DownloadStatus>() {
override fun put(key: String, value: DownloadStatus): DownloadStatus? { override fun put(key: String, value: DownloadStatus): DownloadStatus? {
synchronized(this) { synchronized(this) {
val res = super.put(key, value) val res = super.put(key, value)
emitValue() emitValue(this)
return res return res
} }
} }
@ -25,13 +25,13 @@ class TrackStatusFlowMap(
super.put(title,DownloadStatus.NotDownloaded) super.put(title,DownloadStatus.NotDownloaded)
} }
} }
emitValue() emitValue(this)
//super.clear() super.clear()
//emitValue() emitValue(this)
} }
} }
private fun emitValue() { private fun emitValue(map: HashMap<String,DownloadStatus>) {
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) } scope?.launch { statusFlow.emit(map) }
} }
} }

View File

@ -63,7 +63,7 @@ import com.shabinder.common.uikit.screens.splash.Splash
import com.shabinder.common.uikit.screens.splash.SplashState import com.shabinder.common.uikit.screens.splash.SplashState
import com.shabinder.common.uikit.utils.verticalGradientScrim import com.shabinder.common.uikit.utils.verticalGradientScrim
// To Not Show Splash Again After Configuration Change in Android // Splash Status
private var isSplashShown = SplashState.Show private var isSplashShown = SplashState.Show
@Composable @Composable

View File

@ -10,7 +10,7 @@ kotlin {
dependencies { dependencies {
implementation(project(":common:data-models")) implementation(project(":common:data-models"))
implementation(project(":common:database")) implementation(project(":common:database"))
implementation("org.jetbrains.kotlinx:atomicfu:0.16.2") api("org.jetbrains.kotlinx:atomicfu:0.16.2")
api(MultiPlatformSettings.dep) api(MultiPlatformSettings.dep)
implementation(MVIKotlin.rx) implementation(MVIKotlin.rx)
} }

View File

@ -62,7 +62,8 @@ class ParallelExecutor(
private var service: Job = SupervisorJob() private var service: Job = SupervisorJob()
override val coroutineContext get() = context + service override val coroutineContext get() = context + service
private var isClosed = atomic(false) var isClosed = atomic(false)
private set
private var killQueue = Channel<Unit>(Channel.UNLIMITED) private var killQueue = Channel<Unit>(Channel.UNLIMITED)
private var operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS) private var operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS)
private var concurrentOperationLimit = atomic(concurrentOperationLimit) private var concurrentOperationLimit = atomic(concurrentOperationLimit)
@ -132,6 +133,7 @@ class ParallelExecutor(
} }
// TODO This launches all coroutines in advance even if they're never needed. Find a lazy way to do this. // TODO This launches all coroutines in advance even if they're never needed. Find a lazy way to do this.
@Suppress("unused")
fun setConcurrentOperationLimit(limit: Int) { fun setConcurrentOperationLimit(limit: Int) {
require(limit >= 1) { "'limit' must be greater than zero: $limit" } require(limit >= 1) { "'limit' must be greater than zero: $limit" }
require(limit < 1_000_000) { "Don't use a very high limit because it will cause a lot of coroutines to be started eagerly: $limit" } require(limit < 1_000_000) { "Don't use a very high limit because it will cause a lot of coroutines to be started eagerly: $limit" }

View File

@ -1,6 +1,10 @@
package com.shabinder.common.utils package com.shabinder.common.utils
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.dispatcherIO
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@ -22,4 +26,13 @@ fun StringBuilder.appendPadded(data: Any?) {
fun StringBuilder.appendPadded(header: Any?, data: Any?) { fun StringBuilder.appendPadded(header: Any?, data: Any?) {
appendLine().append(header).appendLine(data).appendLine() appendLine().append(header).appendLine(data).appendLine()
} }
suspend fun <T> runOnMain(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.Main, block)
suspend fun <T> runOnIO(block: suspend CoroutineScope.() -> T): T =
withContext(dispatcherIO, block)
suspend fun <T> runOnDefault(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.Default, block)

View File

@ -50,7 +50,7 @@ internal class SpotiFlyerListImpl(
private val cache = Cache.Builder private val cache = Cache.Builder
.newBuilder() .newBuilder()
.maximumCacheSize(75) .maximumCacheSize(30)
.build<String, Picture>() .build<String, Picture>()
override val model: Value<State> = store.asValue() override val model: Value<State> = store.asValue()

View File

@ -23,9 +23,16 @@ import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.list.SpotiFlyerList.State import com.shabinder.common.list.SpotiFlyerList.State
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.models.* import com.shabinder.common.models.Actions
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.providers.downloadTracks import com.shabinder.common.providers.downloadTracks
import com.shabinder.common.utils.runOnDefault
import com.shabinder.common.utils.runOnMain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependencies) : internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependencies) :
SpotiFlyerList.Dependencies by dependencies { SpotiFlyerList.Dependencies by dependencies {
@ -41,7 +48,11 @@ internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependen
) {} ) {}
private sealed class Result { private sealed class Result {
data class ResultFetched(val result: PlatformQueryResult, val trackList: List<TrackDetails>) : Result() data class ResultFetched(
val result: PlatformQueryResult,
val trackList: List<TrackDetails>
) : Result()
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()
@ -52,79 +63,102 @@ internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependen
override suspend fun executeAction(action: Unit, getState: () -> State) { override suspend fun executeAction(action: Unit, getState: () -> State) {
executeIntent(Intent.SearchLink(link), getState) executeIntent(Intent.SearchLink(link), getState)
runOnDefault {
fileManager.db?.downloadRecordDatabaseQueries?.getLastInsertId()
?.executeAsOneOrNull()?.also {
// See if It's Time we can request for support for maintaining this project or not
fetchQuery.logger.d(
message = { "Database List Last ID: $it" },
tag = "Database Last ID"
)
val offset = preferenceManager.getDonationOffset
dispatchOnMain(
Result.AskForSupport(
// Every 3rd Interval or After some offset
isAllowed = offset < 4 && (it % offset == 0L)
)
)
}
fileManager.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also { downloadProgressFlow.collect { map ->
// See if It's Time we can request for support for maintaining this project or not // logger.d(map.size.toString(), "ListStore: flow Updated")
fetchQuery.logger.d(message = { "Database List Last ID: $it" }, tag = "Database Last ID") getState().trackList.updateTracksStatuses(map).also {
val offset = preferenceManager.getDonationOffset if (it.isNotEmpty())
dispatch( dispatchOnMain(Result.UpdateTrackList(it))
Result.AskForSupport( }
// Every 3rd Interval or After some offset }
isAllowed = offset < 4 && (it % offset == 0L)
)
)
}
downloadProgressFlow.collect { map ->
// logger.d(map.size.toString(), "ListStore: flow Updated")
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
} }
} }
override suspend fun executeIntent(intent: Intent, getState: () -> State) { override suspend fun executeIntent(intent: Intent, getState: () -> State) {
when (intent) { withContext(Dispatchers.Default) {
is Intent.SearchLink -> { when (intent) {
val resp = fetchQuery.query(link) is Intent.SearchLink -> {
resp.fold( val resp = fetchQuery.query(link)
success = { result -> resp.fold(
result.trackList = result.trackList.toMutableList() success = { result ->
dispatch( result.trackList =
(Result.ResultFetched( result.trackList.toMutableList()
result, .updateTracksStatuses(
result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }) downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }
)) )
)
executeIntent(Intent.RefreshTracksStatuses, getState)
},
failure = {
dispatch(Result.ErrorOccurred(it))
}
)
}
is Intent.StartDownloadAll -> { dispatchOnMain(
val list = intent.trackList.map { (Result.ResultFetched(
if (it.downloaded is DownloadStatus.NotDownloaded || it.downloaded is DownloadStatus.Failed) result,
return@map it.copy(downloaded = DownloadStatus.Queued) result.trackList
it ))
} )
dispatch( executeIntent(Intent.RefreshTracksStatuses, getState)
Result.UpdateTrackList( },
list.updateTracksStatuses( failure = {
downloadProgressFlow.replayCache.getOrElse( dispatchOnMain(Result.ErrorOccurred(it))
0 }
) { hashMapOf() })
) )
) }
val finalList = intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded } is Intent.StartDownloadAll -> {
if (finalList.isEmpty()) Actions.instance.showPopUpMessage("All Songs are Processed") val list = intent.trackList.map {
else downloadTracks(finalList, fetchQuery, fileManager) if (it.downloaded is DownloadStatus.NotDownloaded || it.downloaded is DownloadStatus.Failed)
return@map it.copy(downloaded = DownloadStatus.Queued)
it
}
dispatchOnMain(
Result.UpdateTrackList(
list.updateTracksStatuses(
downloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() })
)
)
val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
if (finalList.isEmpty()) Actions.instance.showPopUpMessage("All Songs are Processed")
else downloadTracks(finalList, fetchQuery, fileManager)
}
is Intent.StartDownload -> {
dispatchOnMain(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
downloadTracks(listOf(intent.track), fetchQuery, fileManager)
}
is Intent.RefreshTracksStatuses -> Actions.instance.queryActiveTracks()
} }
is Intent.StartDownload -> {
dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
downloadTracks(listOf(intent.track), fetchQuery, fileManager)
}
is Intent.RefreshTracksStatuses -> Actions.instance.queryActiveTracks()
} }
} }
private suspend fun dispatchOnMain(result: Result) = runOnMain { dispatch(result) }
} }
private object ReducerImpl : Reducer<State, Result> { private object ReducerImpl : Reducer<State, Result> {
override fun State.reduce(result: Result): State = override fun State.reduce(result: Result): State =
when (result) { when (result) {
is Result.ResultFetched -> copy(queryResult = result.result, trackList = result.trackList, link = link) is Result.ResultFetched -> copy(
queryResult = result.result,
trackList = result.trackList,
link = link
)
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)
@ -158,7 +192,6 @@ internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependen
} }
} }
} }
return updatedList return updatedList
} }
} }

View File

@ -49,7 +49,7 @@ internal class SpotiFlyerMainImpl(
private val cache = Cache.Builder private val cache = Cache.Builder
.newBuilder() .newBuilder()
.maximumCacheSize(25) .maximumCacheSize(20)
.build<String, Picture>() .build<String, Picture>()
override val model: Value<State> = store.asValue() override val model: Value<State> = store.asValue()

View File

@ -25,6 +25,7 @@ import com.shabinder.common.main.SpotiFlyerMain.State
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
import com.shabinder.common.models.DownloadRecord import com.shabinder.common.models.DownloadRecord
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.utils.runOnMain
import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers