mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
Service Cleanup, AutoClear & Notification Optimisations
This commit is contained in:
parent
b3abc9c4de
commit
6566c35888
@ -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")
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.shabinder.spotiflyer.utils.autoclear
|
||||||
|
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
|
||||||
|
interface LifecycleAutoInitializer<T>: DefaultLifecycleObserver {
|
||||||
|
var value: T?
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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)
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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--
|
||||||
|
Loading…
Reference in New Issue
Block a user