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) {
implementation("androidx.lifecycle:lifecycle-service:$this")
implementation("androidx.lifecycle:lifecycle-common-java8:$this")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$this")
}
implementation(Extras.kermit)

View File

@ -40,7 +40,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.defaultComponentContext
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.PermissionDialog
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
@ -204,8 +208,10 @@ class MainActivity : ComponentActivity() {
foregroundService = binder.service
isServiceBound = true
lifecycleScope.launch {
foregroundService?.trackStatusFlowMap?.statusFlow?.let {
trackStatusFlow.emitAll(it.conflate())
repeatOnLifecycle(Lifecycle.State.STARTED) {
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.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.content.Context
import android.content.Intent
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.translations.Strings
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.utils.autoclear.autoClear
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
@ -57,12 +54,11 @@ import java.io.File
class ForegroundService : LifecycleService() {
private lateinit var downloadService: ParallelExecutor
val trackStatusFlowMap by autoClear {
TrackStatusFlowMap(
MutableSharedFlow(replay = 1),
lifecycleScope
)
}
val trackStatusFlowMap = TrackStatusFlowMap(
MutableSharedFlow(replay = 1),
lifecycleScope
)
private val fetcher: FetchPlatformQueryResult by inject()
private val logger: Kermit by inject()
private val dir: FileManager by inject()
@ -73,7 +69,12 @@ class ForegroundService : LifecycleService() {
private var isServiceStarted = false
private val cancelIntent: PendingIntent by lazy {
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 */
@ -98,6 +99,7 @@ class ForegroundService : LifecycleService() {
override fun onCreate() {
super.onCreate()
downloadService = ParallelExecutor(Dispatchers.IO)
trackStatusFlowMap.scope = lifecycleScope
createNotificationChannel(CHANNEL_ID, "Downloader Service")
}
@ -271,12 +273,16 @@ class ForegroundService : LifecycleService() {
private fun killService() {
lifecycleScope.launch {
logger.d(TAG) { "Killing Self" }
resetVar()
messageList = messageList.getEmpty().apply {
set(index = 0, Message(Strings.cleaningAndExiting(), DownloadStatus.NotDownloaded))
}
downloadService.close()
updateNotification()
trackStatusFlowMap.clear()
trackStatusFlowMap.apply {
clear()
scope = null
}
cleanFiles(File(dir.defaultDir()))
// cleanFiles(File(dir.imageCacheDir()))
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 =
NotificationCompat.Builder(this, CHANNEL_ID).run {
setSmallIcon(R.drawable.ic_download_arrow)
@ -323,6 +336,7 @@ class ForegroundService : LifecycleService() {
updateNotification()
}
@Suppress("unused")
private fun updateProgressInNotification(message: Message) {
synchronized(messageList) {
val index = messageList.indexOfFirst { it.title == message.title }
@ -331,10 +345,16 @@ class ForegroundService : LifecycleService() {
updateNotification()
}
// Update Notification only if Service is Still Active
private fun updateNotification() {
val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(NOTIFICATION_ID, createNotification())
if (!downloadService.isClosed.value) {
val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(NOTIFICATION_ID, createNotification())
} else {
// Service is Inactive so clear status
resetVar()
}
}
override fun onDestroy() {

View File

@ -7,12 +7,12 @@ import kotlinx.coroutines.launch
class TrackStatusFlowMap(
val statusFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>,
private val scope: CoroutineScope
var scope: CoroutineScope?
) : HashMap<String, DownloadStatus>() {
override fun put(key: String, value: DownloadStatus): DownloadStatus? {
synchronized(this) {
val res = super.put(key, value)
emitValue()
emitValue(this)
return res
}
}
@ -25,13 +25,13 @@ class TrackStatusFlowMap(
super.put(title,DownloadStatus.NotDownloaded)
}
}
emitValue()
//super.clear()
//emitValue()
emitValue(this)
super.clear()
emitValue(this)
}
}
private fun emitValue() {
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }
private fun emitValue(map: HashMap<String,DownloadStatus>) {
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.utils.verticalGradientScrim
// To Not Show Splash Again After Configuration Change in Android
// Splash Status
private var isSplashShown = SplashState.Show
@Composable

View File

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

View File

@ -62,7 +62,8 @@ class ParallelExecutor(
private var service: Job = SupervisorJob()
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 operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS)
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.
@Suppress("unused")
fun setConcurrentOperationLimit(limit: Int) {
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" }

View File

@ -1,6 +1,10 @@
package com.shabinder.common.utils
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.InvocationKind
import kotlin.contracts.contract
@ -22,4 +26,13 @@ fun StringBuilder.appendPadded(data: Any?) {
fun StringBuilder.appendPadded(header: Any?, data: Any?) {
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
.newBuilder()
.maximumCacheSize(75)
.maximumCacheSize(30)
.build<String, Picture>()
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.State
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.utils.runOnDefault
import com.shabinder.common.utils.runOnMain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependencies) :
SpotiFlyerList.Dependencies by dependencies {
@ -41,7 +48,11 @@ internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependen
) {}
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 UpdateTrackItem(val item: TrackDetails) : 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) {
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 {
// 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
dispatch(
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))
downloadProgressFlow.collect { map ->
// logger.d(map.size.toString(), "ListStore: flow Updated")
getState().trackList.updateTracksStatuses(map).also {
if (it.isNotEmpty())
dispatchOnMain(Result.UpdateTrackList(it))
}
}
}
}
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
when (intent) {
is Intent.SearchLink -> {
val resp = fetchQuery.query(link)
resp.fold(
success = { result ->
result.trackList = result.trackList.toMutableList()
dispatch(
(Result.ResultFetched(
result,
result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })
))
)
executeIntent(Intent.RefreshTracksStatuses, getState)
},
failure = {
dispatch(Result.ErrorOccurred(it))
}
)
}
withContext(Dispatchers.Default) {
when (intent) {
is Intent.SearchLink -> {
val resp = fetchQuery.query(link)
resp.fold(
success = { result ->
result.trackList =
result.trackList.toMutableList()
.updateTracksStatuses(
downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }
)
is Intent.StartDownloadAll -> {
val list = intent.trackList.map {
if (it.downloaded is DownloadStatus.NotDownloaded || it.downloaded is DownloadStatus.Failed)
return@map it.copy(downloaded = DownloadStatus.Queued)
it
}
dispatch(
Result.UpdateTrackList(
list.updateTracksStatuses(
downloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() })
dispatchOnMain(
(Result.ResultFetched(
result,
result.trackList
))
)
executeIntent(Intent.RefreshTracksStatuses, getState)
},
failure = {
dispatchOnMain(Result.ErrorOccurred(it))
}
)
)
}
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.StartDownloadAll -> {
val list = intent.trackList.map {
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> {
override fun State.reduce(result: Result): State =
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.UpdateTrackItem -> updateTrackItem(result.item)
is Result.ErrorOccurred -> copy(errorOccurred = result.error)
@ -158,7 +192,6 @@ internal class SpotiFlyerListStoreProvider(dependencies: SpotiFlyerList.Dependen
}
}
}
return updatedList
}
}

View File

@ -49,7 +49,7 @@ internal class SpotiFlyerMainImpl(
private val cache = Cache.Builder
.newBuilder()
.maximumCacheSize(25)
.maximumCacheSize(20)
.build<String, Picture>()
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.models.DownloadRecord
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.mapToList
import kotlinx.coroutines.Dispatchers