Service Cleanup, AutoClear & Notification Optimisations

This commit is contained in:
shabinder 2021-06-23 00:18:01 +05:30
parent b3abc9c4de
commit 6566c35888
14 changed files with 343 additions and 89 deletions

View File

@ -128,11 +128,16 @@ dependencies {
implementation(matomo) implementation(matomo)
} }
with(Versions.androidxLifecycle) {
implementation("androidx.lifecycle:lifecycle-service:$this")
implementation("androidx.lifecycle:lifecycle-common-java8:$this")
}
implementation(Extras.kermit) 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")
implementation("com.google.accompanist:accompanist-insets:0.11.1") implementation("com.google.accompanist:accompanist-insets:0.12.0")
// Test // Test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")

View File

@ -17,13 +17,11 @@
package com.shabinder.spotiflyer.service package com.shabinder.spotiflyer.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.DownloadManager
import android.app.Notification 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.app.PendingIntent.FLAG_CANCEL_CURRENT
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.Binder
@ -31,8 +29,9 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.FetchPlatformQueryResult
@ -44,23 +43,33 @@ import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.event.coroutines.failure import com.shabinder.common.models.event.coroutines.failure
import kotlinx.coroutines.CoroutineScope import com.shabinder.spotiflyer.utils.autoclear.AutoClear
import com.shabinder.spotiflyer.utils.autoclear.autoClear
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow 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
import java.io.File import java.io.File
import kotlin.coroutines.CoroutineContext
class ForegroundService : Service(), CoroutineScope { class ForegroundService : LifecycleService() {
private val tag: String = "Foreground Service" val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1),lifecycleScope) }
private val channelId = "ForegroundDownloaderService" private var downloadService: AutoClear<ParallelExecutor> = autoClear { ParallelExecutor(Dispatchers.IO) }
private val notificationId = 101 private val fetcher: FetchPlatformQueryResult by inject()
private val logger: Kermit by inject()
private val dir: Dir by inject()
private var messageList = MutableList(5) { emptyMessage }
private var wakeLock: PowerManager.WakeLock? = null
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)
}
/* Variables Holding Download State */
private var total = 0 // Total Downloads Requested private var total = 0 // Total Downloads Requested
private var converted = 0 // Total Files Converted private var converted = 0 // Total Files Converted
private var downloaded = 0 // Total Files downloaded private var downloaded = 0 // Total Files downloaded
@ -68,52 +77,27 @@ class ForegroundService : Service(), CoroutineScope {
private val isFinished get() = converted + failed == total private val isFinished get() = converted + failed == total
private var isSingleDownload = false private var isSingleDownload = false
private lateinit var serviceJob: Job
override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO
val trackStatusFlowMap = TrackStatusFlowMap(MutableSharedFlow(replay = 1),this)
private var messageList = mutableListOf("", "", "", "", "")
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
private lateinit var cancelIntent: PendingIntent
private lateinit var downloadManager: DownloadManager
private lateinit var downloadService: ParallelExecutor
private val fetcher: FetchPlatformQueryResult by inject()
private val logger: Kermit by inject()
private val dir: Dir by inject()
inner class DownloadServiceBinder : Binder() { inner class DownloadServiceBinder : Binder() {
// Return this instance of MyService so clients can call public methods val service get() = this@ForegroundService
val service: ForegroundService
get() =// Return this instance of Foreground Service so clients can call public methods
this@ForegroundService
} }
private val myBinder: IBinder = DownloadServiceBinder() private val myBinder: IBinder = DownloadServiceBinder()
override fun onBind(intent: Intent): IBinder = myBinder override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return myBinder
}
@SuppressLint("UnspecifiedImmutableFlag")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
serviceJob = SupervisorJob() createNotificationChannel(CHANNEL_ID, "Downloader Service")
downloadService = ParallelExecutor(Dispatchers.IO)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, "Downloader Service")
}
val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" }
cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT)
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
// Send a notification that service is started // Send a notification that service is started
Log.i(tag, "Foreground Service Started.") Log.i(TAG, "Foreground Service Started.")
startForeground(notificationId, getNotification()) startForeground(NOTIFICATION_ID, getNotification())
intent?.let { intent?.let {
when (it.action) { when (it.action) {
@ -127,7 +111,7 @@ class ForegroundService : Service(), CoroutineScope {
START_STICKY START_STICKY
} else { } else {
isServiceStarted = true isServiceStarted = true
Log.i(tag, "Starting the foreground service task") Log.i(TAG, "Starting the foreground service task")
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
@ -142,7 +126,6 @@ class ForegroundService : Service(), CoroutineScope {
* Function To Download All Tracks Available in a List * Function To Download All Tracks Available in a List
**/ **/
fun downloadAllTracks(trackList: List<TrackDetails>) { fun downloadAllTracks(trackList: List<TrackDetails>) {
trackList.size.also { size -> trackList.size.also { size ->
total += size total += size
isSingleDownload = (size == 1) isSingleDownload = (size == 1)
@ -151,8 +134,8 @@ class ForegroundService : Service(), CoroutineScope {
trackList.forEach { trackList.forEach {
trackStatusFlowMap[it.title] = DownloadStatus.Queued trackStatusFlowMap[it.title] = DownloadStatus.Queued
launch { lifecycleScope.launch {
downloadService.execute { downloadService.value.execute {
fetcher.findMp3DownloadLink(it).fold( fetcher.findMp3DownloadLink(it).fold(
success = { url -> success = { url ->
enqueueDownload(url, it) enqueueDownload(url, it)
@ -170,21 +153,22 @@ class ForegroundService : Service(), CoroutineScope {
private suspend fun enqueueDownload(url: String, track: TrackDetails) { private suspend fun enqueueDownload(url: String, track: TrackDetails) {
// Initiating Download // Initiating Download
addToNotification("Downloading ${track.title}") addToNotification(Message(track.title, DownloadStatus.Downloading()))
trackStatusFlowMap[track.title] = DownloadStatus.Downloading() trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
// Enqueueing Download // Enqueueing Download
downloadFile(url).collect { downloadFile(url).collect {
when (it) { when (it) {
is DownloadResult.Error -> { is DownloadResult.Error -> {
logger.d(tag) { it.message } logger.d(TAG) { it.message }
failed++ failed++
trackStatusFlowMap[track.title] = DownloadStatus.Failed(it.cause ?: Exception(it.message)) trackStatusFlowMap[track.title] = DownloadStatus.Failed(it.cause ?: Exception(it.message))
removeFromNotification("Downloading ${track.title}") removeFromNotification(Message(track.title, DownloadStatus.Downloading()))
} }
is DownloadResult.Progress -> { is DownloadResult.Progress -> {
trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress) trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress)
updateProgressInNotification(Message(track.title,DownloadStatus.Downloading(it.progress)))
} }
is DownloadResult.Success -> { is DownloadResult.Success -> {
@ -195,15 +179,15 @@ class ForegroundService : Service(), CoroutineScope {
// Send Converting Status // Send Converting Status
trackStatusFlowMap[track.title] = DownloadStatus.Converting trackStatusFlowMap[track.title] = DownloadStatus.Converting
addToNotification("Processing ${track.title}") addToNotification(Message(track.title, DownloadStatus.Converting))
// All Processing Completed for this Track // All Processing Completed for this Track
job.invokeOnCompletion { job.invokeOnCompletion {
converted++ converted++
trackStatusFlowMap[track.title] = DownloadStatus.Downloaded trackStatusFlowMap[track.title] = DownloadStatus.Downloaded
removeFromNotification("Processing ${track.title}") removeFromNotification(Message(track.title, DownloadStatus.Converting))
} }
logger.d(tag) { "${track.title} Download Completed" } logger.d(TAG) { "${track.title} Download Completed" }
downloaded++ downloaded++
}.failure { error -> }.failure { error ->
error.printStackTrace() error.printStackTrace()
@ -211,7 +195,7 @@ class ForegroundService : Service(), CoroutineScope {
failed++ failed++
trackStatusFlowMap[track.title] = DownloadStatus.Failed(error) trackStatusFlowMap[track.title] = DownloadStatus.Failed(error)
} }
removeFromNotification("Downloading ${track.title}") removeFromNotification(Message(track.title, DownloadStatus.Downloading()))
} }
} }
} }
@ -219,7 +203,7 @@ class ForegroundService : Service(), CoroutineScope {
} }
private fun releaseWakeLock() { private fun releaseWakeLock() {
logger.d(tag) { "Releasing Wake Lock" } logger.d(TAG) { "Releasing Wake Lock" }
try { try {
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
@ -227,14 +211,14 @@ class ForegroundService : Service(), CoroutineScope {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.d(tag) { "Service stopped without being started: ${e.message}" } logger.d(TAG) { "Service stopped without being started: ${e.message}" }
} }
isServiceStarted = false isServiceStarted = false
} }
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String) { private fun createNotificationChannel(channelId: String, channelName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel( val channel = NotificationChannel(
channelId, channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT channelName, NotificationManager.IMPORTANCE_DEFAULT
@ -243,22 +227,25 @@ class ForegroundService : Service(), CoroutineScope {
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(channel) service.createNotificationChannel(channel)
} }
}
/* /*
* Time To Wrap UP * Time To Wrap UP
* - `Clean Up` and `Stop this Foreground Service` * - `Clean Up` and `Stop this Foreground Service`
* */ * */
private fun killService() { private fun killService() {
launch { lifecycleScope.launch {
logger.d(tag) { "Killing Self" } logger.d(TAG) { "Killing Self" }
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") messageList = messageList.getEmpty().apply {
downloadService.close() set(index = 0, Message("Cleaning And Exiting",DownloadStatus.NotDownloaded))
}
downloadService.value.close()
downloadService.reset()
updateNotification() updateNotification()
cleanFiles(File(dir.defaultDir())) cleanFiles(File(dir.defaultDir()))
// TODO cleanFiles(File(dir.imageCacheDir())) // cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("", "", "", "", "") messageList = messageList.getEmpty()
releaseWakeLock() releaseWakeLock()
serviceJob.cancel()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true) stopForeground(true)
stopSelf() stopSelf()
@ -270,6 +257,7 @@ class ForegroundService : Service(), CoroutineScope {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
logger.i(TAG) { "onDestroy, isFinished: $isFinished" }
if (isFinished) { if (isFinished) {
killService() killService()
} }
@ -277,6 +265,7 @@ class ForegroundService : Service(), CoroutineScope {
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
logger.i(TAG) { "onTaskRemoved, isFinished: $isFinished" }
if (isFinished) { if (isFinished) {
killService() killService()
} }
@ -285,30 +274,39 @@ class ForegroundService : Service(), CoroutineScope {
/* /*
* Create A New Notification with all the updated data * Create A New Notification with all the updated data
* */ * */
private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run { private fun getNotification(): Notification = NotificationCompat.Builder(this, CHANNEL_ID).run {
setSmallIcon(R.drawable.ic_download_arrow) setSmallIcon(R.drawable.ic_download_arrow)
setContentTitle("Total: $total Completed:$converted Failed:$failed") setContentTitle("Total: $total Completed:$converted Failed:$failed")
setSilent(true) setSilent(true)
// val max
// val progress = if(total != 0) 0 else (((failed+converted).toDouble() / total.toDouble()).roundToInt())
setProgress(total,failed+converted,false)
setStyle( setStyle(
NotificationCompat.InboxStyle().run { NotificationCompat.InboxStyle().run {
addLine(messageList[messageList.size - 1]) addLine(messageList[messageList.size - 1].asString())
addLine(messageList[messageList.size - 2]) addLine(messageList[messageList.size - 2].asString())
addLine(messageList[messageList.size - 3]) addLine(messageList[messageList.size - 3].asString())
addLine(messageList[messageList.size - 4]) addLine(messageList[messageList.size - 4].asString())
addLine(messageList[messageList.size - 5]) addLine(messageList[messageList.size - 5].asString())
} }
) )
addAction(R.drawable.ic_round_cancel_24, "Exit", cancelIntent) addAction(R.drawable.ic_round_cancel_24, "Exit", cancelIntent)
build() build()
} }
private fun addToNotification(message: String) { private fun addToNotification(message: Message) {
messageList.add(message) messageList.add(message)
updateNotification() updateNotification()
} }
private fun removeFromNotification(message: String) { private fun removeFromNotification(message: Message) {
messageList.remove(message) messageList.removeAll { it.title == message.title }
updateNotification()
}
private fun updateProgressInNotification(message: Message) {
val index = messageList.indexOfFirst { it.title == message.title }
messageList[index] = message
updateNotification() updateNotification()
} }
@ -318,6 +316,13 @@ class ForegroundService : Service(), CoroutineScope {
private fun updateNotification() { private fun updateNotification() {
val mNotificationManager: NotificationManager = val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.notify(notificationId, getNotification()) mNotificationManager.notify(NOTIFICATION_ID, getNotification())
} }
companion object {
private const val TAG: String = "Foreground Service"
private const val CHANNEL_ID = "ForegroundDownloaderService"
private const val NOTIFICATION_ID = 101
}
} }

View File

@ -0,0 +1,33 @@
package com.shabinder.spotiflyer.service
import com.shabinder.common.models.DownloadStatus
typealias Message = Pair<String, DownloadStatus>
val Message.title: String get() = first
val Message.downloadStatus: DownloadStatus get() = second
val Message.progress: String get() = when (downloadStatus) {
is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%"
is DownloadStatus.Converting -> "-> 100%"
is DownloadStatus.Downloaded -> "-> Done"
is DownloadStatus.Failed -> "-> Failed"
is DownloadStatus.Queued -> "-> Queued"
is DownloadStatus.NotDownloaded -> ""
}
val emptyMessage = Message("",DownloadStatus.NotDownloaded)
// `Progress` is not being shown because we don't get get consistent Updates from Download Fun ,
// all Progress data is emitted all together from fun
fun Message.asString(): String {
val statusString = when(downloadStatus){
is DownloadStatus.Downloading -> "Downloading"
is DownloadStatus.Converting -> "Processing"
else -> ""
}
return "$statusString $title ${""/*progress*/}".trim()
}
fun List<Message>.getEmpty(): MutableList<Message> = MutableList(size) { emptyMessage }

View File

@ -0,0 +1,74 @@
package com.shabinder.spotiflyer.utils.autoclear
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.shabinder.common.requireNotNull
import com.shabinder.spotiflyer.utils.autoclear.AutoClear.Companion.TRIGGER
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleCreateAndDestroyObserver
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleResumeAndPauseObserver
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleStartAndStopObserver
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class AutoClear<T : Any?>(
lifecycle: Lifecycle,
private val initializer: (() -> T)?,
private val trigger: TRIGGER = TRIGGER.ON_CREATE,
) : ReadWriteProperty<LifecycleOwner, T?> {
companion object {
enum class TRIGGER {
ON_CREATE,
ON_START,
ON_RESUME
}
}
private var _value: T?
get() = observer.value
set(value) { observer.value = value }
val value: T get() = _value.requireNotNull()
private val observer: LifecycleAutoInitializer<T?> by lazy {
when(trigger) {
TRIGGER.ON_CREATE -> LifecycleCreateAndDestroyObserver(initializer)
TRIGGER.ON_START -> LifecycleStartAndStopObserver(initializer)
TRIGGER.ON_RESUME -> LifecycleResumeAndPauseObserver(initializer)
}
}
init {
lifecycle.addObserver(observer)
}
override fun getValue(thisRef: LifecycleOwner, property: KProperty<*>): T {
if (_value != null) {
return value
}
// If for Some Reason Initializer is not invoked even after Initialisation, invoke it after checking state
if (thisRef.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
return initializer?.invoke().also { _value = it }
?: throw IllegalStateException("The value has not yet been set or no default initializer provided")
} else {
throw IllegalStateException("Activity might have been destroyed or not initialized yet")
}
}
override fun setValue(thisRef: LifecycleOwner, property: KProperty<*>, value: T?) {
this._value = value
}
fun reset() {
this._value = null
}
}
fun <T : Any> LifecycleOwner.autoClear(
trigger: TRIGGER = TRIGGER.ON_CREATE,
initializer: () -> T
): AutoClear<T> {
return AutoClear(this.lifecycle, initializer, trigger)
}

View File

@ -0,0 +1,62 @@
package com.shabinder.spotiflyer.utils.autoclear
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class AutoClearFragment<T : Any?>(
fragment: Fragment,
private val initializer: (() -> T)?
) : ReadWriteProperty<Fragment, T?> {
private var _value: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
val viewLifecycleOwnerObserver = Observer<LifecycleOwner?> { viewLifecycleOwner ->
viewLifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_value = null
}
})
}
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver)
}
override fun onDestroy(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver)
}
}
)
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val value = _value
if (value != null) {
return value
}
if (thisRef.viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
return initializer?.invoke().also { _value = it }
?: throw IllegalStateException("The value has not yet been set or no default initializer provided")
} else {
throw IllegalStateException("Fragment might have been destroyed or not initialized yet")
}
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) {
_value = value
}
}
fun <T : Any> Fragment.autoClear(initializer: () -> T): AutoClearFragment<T> {
return AutoClearFragment(this, initializer)
}

View File

@ -0,0 +1,7 @@
package com.shabinder.spotiflyer.utils.autoclear
import androidx.lifecycle.DefaultLifecycleObserver
interface LifecycleAutoInitializer<T>: DefaultLifecycleObserver {
var value: T?
}

View File

@ -0,0 +1,21 @@
package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
import androidx.lifecycle.LifecycleOwner
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
class LifecycleCreateAndDestroyObserver<T: Any?>(
private val initializer: (() -> T)?
) : LifecycleAutoInitializer<T> {
override var value: T? = null
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
value = initializer?.invoke()
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
value = null
}
}

View File

@ -0,0 +1,21 @@
package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
import androidx.lifecycle.LifecycleOwner
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
class LifecycleResumeAndPauseObserver<T: Any?>(
private val initializer: (() -> T)?
) : LifecycleAutoInitializer<T> {
override var value: T? = null
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
value = initializer?.invoke()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
value = null
}
}

View File

@ -0,0 +1,21 @@
package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
import androidx.lifecycle.LifecycleOwner
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
class LifecycleStartAndStopObserver<T: Any?>(
private val initializer: (() -> T)?
) : LifecycleAutoInitializer<T> {
override var value: T? = null
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
value = initializer?.invoke()
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
value = null
}
}

View File

@ -49,7 +49,7 @@ object Versions {
const val minSdkVersion = 21 const val minSdkVersion = 21
const val compileSdkVersion = 29 const val compileSdkVersion = 29
const val targetSdkVersion = 29 const val targetSdkVersion = 29
const val androidLifecycle = "2.3.0" const val androidxLifecycle = "2.3.1"
} }
object HostOS { object HostOS {

View File

@ -1,3 +1,3 @@
package com.shabinder.common package com.shabinder.common
fun <T: Any> T?.requireNotNull() : T = requireNotNull(this) fun <T: Any?> T?.requireNotNull() : T = requireNotNull(this)

View File

@ -4,7 +4,11 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag
data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message) data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message)
data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message) data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message)
data class MP3ConversionFailed(override val message: String = "MP3 Converter unreachable, probably BUSY !"): SpotiFlyerException(message)
data class MP3ConversionFailed(
val extraInfo:String? = null,
override val message: String = "MP3 Converter unreachable, probably BUSY ! \nCAUSE:$extraInfo"
): SpotiFlyerException(message)
data class NoMatchFound( data class NoMatchFound(
val trackName: String? = null, val trackName: String? = null,

View File

@ -122,6 +122,7 @@ class FetchPlatformQueryResult(
trackName = track.title, trackName = track.title,
trackArtists = track.artists trackArtists = track.artists
).flatMapError { saavnError -> ).flatMapError { saavnError ->
logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" }
// Saavn Failed, Lets Try Fetching Now From Youtube Music // Saavn Failed, Lets Try Fetching Now From Youtube Music
youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError -> youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError ->
// If Both Failed Bubble the Exception Up with both StackTraces // If Both Failed Bubble the Exception Up with both StackTraces

View File

@ -47,16 +47,16 @@ interface AudioToMp3 {
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}" "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
if(e is ClientRequestException && e.response.status.value == 404) { if(e is ClientRequestException && e.response.status.value == 404) {
// No Need to Retry, Host/Converter is Busy // No Need to Retry, Host/Converter is Busy
throw SpotiFlyerException.MP3ConversionFailed() throw SpotiFlyerException.MP3ConversionFailed(e.message)
} }
// Try Using New Host/Converter // Try Using New Host/Converter
convertRequest(URL, audioQuality).value.also { convertRequest(URL, audioQuality).value.also {
activeHost = it.first activeHost = it.first
jobLink = it.second jobLink = it.second
} }
e.printStackTrace()
"" ""
} }
retryCount-- retryCount--