I have implemented a synchronous download mechanism for my download TrackDownloadService. I can not trigger the cancelDownload() method to cancel the download when I click "Cancel" from my notification. How to make it work? Please give me some suggestions if I am doing something wrong i my code.
This is the PausingDispatchQueue which is responsible for pause and resume functionality:
class PausingDispatchQueue : AbstractCoroutineContextElement(Key) {
private val paused = AtomicBoolean(false)
private val queue = ArrayDeque<Resumer>()
val isPaused: Boolean
get() = paused.get()
fun pause() {
paused.set(true)
}
fun resume() {
if (paused.compareAndSet(true, false)) {
dispatchNext()
}
}
fun queue(context: CoroutineContext, block: Runnable, dispatcher: CoroutineDispatcher) {
queue.addLast(Resumer(dispatcher, context, block))
}
private fun dispatchNext() {
val resumer = queue.removeFirstOrNull() ?: return
resumer.dispatch()
}
private inner class Resumer(
private val dispatcher: CoroutineDispatcher,
private val context: CoroutineContext,
private val block: Runnable,
) : Runnable {
override fun run() {
block.run()
if (!paused.get()) {
dispatchNext()
}
}
fun dispatch() {
dispatcher.dispatch(context, this)
}
}
companion object Key : CoroutineContext.Key<PausingDispatchQueue>
}
This is the TracksDownloadService class:
class TrackDownloadService : Service() {
private lateinit var downloadScope: CoroutineScope
private lateinit var notificationManager: NotificationManager
private val CHANNEL_ID = "TrackDownloadChannel"
private val NOTIFICATION_ID = 4
private lateinit var cancelReceiver: DownloadCancelReceiver
private val queue = PausingDispatchQueue()
private val cancelToken = AtomicBoolean(false)
private val downloadFlow = MutableSharedFlow<DownloadEvent>(extraBufferCapacity = 1) // To emit DownloadEvents
private val mBinder: IBinder = MyBinder()
override fun onBind(intent: Intent): IBinder {
return mBinder
}
inner class MyBinder : Binder() {
val service: TrackDownloadService
get() = this@TrackDownloadService
}
companion object {
const val TAG = "TrackDownloadService"
private lateinit var cancelPendingIntent: PendingIntent
// var isDownloadCancelled: Boolean = false
private var instance: TrackDownloadService? = null
fun getInstance(): TrackDownloadService? {
return instance
}
fun stopService(context: Context) {
val intent = Intent(context, TrackDownloadService::class.java)
intent.action = "com.offlinemusicplayer.ACTION_STOP_FOREGROUND_SERVICE"
context.stopService(intent)
}
}
fun startService(context: Context) {
val intent = Intent(context, TrackDownloadService::class.java)
intent.action = "com.offlinemusicplayer.ACTION_CANCEL_DOWNLOAD"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Handle incoming intents if needed
return START_STICKY // Or any other appropriate return value
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "OnCreate $TAG")
createNotificationChannel()
startForeground(NOTIFICATION_ID, createNotification("Starting download..."))
// Register the receiver
cancelReceiver = DownloadCancelReceiver()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(cancelReceiver,
IntentFilter(DownloadCancelReceiver.ACTION_CANCEL_DOWNLOAD),
RECEIVER_NOT_EXPORTED
)
}
}
// Reinitialize the coroutine scope
downloadScope = CoroutineScope(Dispatchers.IO)
initiateDownloadProcess()
}
override fun onDestroy() {
super.onDestroy()
// Cancel the download coroutine scope
Log.d(TAG, "Service onDestroy is called")
unregisterReceiver(cancelReceiver)
// isDownloadCancelled = false
// downloadScope.coroutineContext.cancelChildren()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Track Download",
NotificationManager.IMPORTANCE_LOW
)
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun createNotification(contentText: String): Notification {
val cancelIntent = Intent(this, DownloadCancelReceiver::class.java).apply {
action = DownloadCancelReceiver.ACTION_CANCEL_DOWNLOAD
}
cancelPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(this, 0, cancelIntent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getBroadcast(this, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Track Download")
.setContentText(contentText)
.setSmallIcon(R.drawable.ic_folder_64)
.setProgress(100, 0, false)
.setPriority(NotificationCompat.PRIORITY_LOW)
.addAction(R.drawable.ic_file_delete, "Cancel", cancelPendingIntent)
.build()
}
private fun updateNotification(progress: Int, contentText: String) {
val cancelIntent: Intent = Intent(this, DownloadCancelReceiver::class.java).apply {
action = DownloadCancelReceiver.ACTION_CANCEL_DOWNLOAD
}
cancelPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(this, 0, cancelIntent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getBroadcast(this, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Track Download")
.setContentText(contentText)
.setSmallIcon(R.drawable.ic_folder_64)
.setProgress(100, progress, false)
.setPriority(NotificationCompat.PRIORITY_LOW)
.addAction(R.drawable.ic_file_delete, "Cancel", cancelPendingIntent)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun initiateDownloadProcess() {
downloadScope.launch {
DownloadChannel.downloadEventChannel
.consumeAsFlow()
.flatMapLatest { downloadEvent ->
Log.d(TAG, "Inside initiateDownloadProcess Name: ${downloadEvent.track.name}, headers: ${downloadEvent.response.headers}")
trackDownloadFlow(downloadEvent)
}
.collect { result ->
Log.d(TAG, result)
}
}
}
// Method to create the flow that handles the download event
private fun trackDownloadFlow(downloadEvent: DownloadEvent): Flow<String> = flow {
val downloadResponse = downloadEvent.response
val track = downloadEvent.track
if (downloadResponse.isSuccessful) {
Log.d(TAG, "Download response body headers: ${downloadResponse.headers}")
val body = downloadResponse.body ?: return@flow
val originalContentLength: String? = downloadResponse.header("x-original-content-length")
val fileSize = body.contentLength().takeIf { it != -1L } ?: originalContentLength?.toLong() ?: -1L
Log.d(TAG, "File size: $fileSize")
val inputStream = body.byteStream()
// Specify the path to save the file
val songsFolder = Utils.makeSongsFolder(OfflineMusicApp.instance)
val filePath = File(songsFolder, track.name + UUID.randomUUID())
val outputStream = FileOutputStream(filePath)
val bufferSize = 8192 // 8KB buffer
val data = ByteArray(bufferSize)
var total: Long = 0
var count: Int
var lastProgressUpdate = 0L
while (inputStream.read(data).also { count = it } != -1) {
// Check for cancellation
if (cancelToken.get()) {
Log.d(TAG, "Download canceled!")
outputStream.close()
inputStream.close()
filePath.delete()
emit("Download canceled!")
return@flow
}
// Check for pause
while (queue.isPaused) {
Log.d(TAG, "Download paused... waiting to resume.")
delay(500) // Avoid busy-waiting
}
total += count
val progress = ((total * 100) / fileSize).toInt()
if (progress > lastProgressUpdate + 1 || System.currentTimeMillis() - lastProgressUpdate > 500) {
updateNotification(progress, "Downloading: ${track.name} $progress%")
withContext(Dispatchers.Main) {
DownloadChannel.downloadProgressChannel.send(track.id to progress)
}
lastProgressUpdate = System.currentTimeMillis()
}
outputStream.write(data, 0, count)
}
outputStream.flush()
outputStream.close()
inputStream.close()
val newName = DuplicateCopyManager.getTrackName(track.name)
val dest = File(songsFolder, newName)
filePath.renameTo(dest)
withContext(Dispatchers.Default) {
val trackMetadata = TracksManager().getMetadata(dest.toString())
DBManager.insertTrack(trackMetadata) {
updateNotification(100, "Download completed: ${track.name}")
CoroutineScope(Dispatchers.Main).launch {
DownloadChannel.downloadProgressChannel.send(track.id to 100)
}
}
}
emit("Download completed!")
} else {
emit("Failed to download the file: ${track.name}")
}
}.flowOn(Dispatchers.IO) // Ensure emissions happen on IO dispatcher
fun cancelDownload() {
cancelToken.set(true) // Set the cancellation flag
Log.d(TAG, "Download cancellation requested.")
updateNotification(0, "Download canceled!") // Update the notification
}
}
This is the DonwloadCancelReceiver class to receive the cancel intent
class DownloadCancelReceiver : BroadcastReceiver() {
companion object {
const val ACTION_CANCEL_DOWNLOAD = "com.offlinemusicplayer.ACTION_CANCEL_DOWNLOAD"
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
Log.d("DownloadCancelReceiver", "Cancel download action received.")
// Access the service instance and set the cancel flag
TrackDownloadService.getInstance()?.cancelDownload()
}
}
}
Here, the receiver gets the intent action successfully but cancelDownload() method is not getting called from DownloadCancelReceiver to TrackDownloadService and ultimately cancellation is not working.
TrackDownloadService.getInstance()
will always return null
, because instance
is always null
.
I recommend removing instance
and getInstance()
. With your current architecture, you can pass in TrackDownloadService
via a constructor parameter:
class DownloadCancelReceiver(service: TrackDownloadService) : BroadcastReceiver() {
// rest of code goes here
}
cancelReceiver = DownloadCancelReceiver(this)
Personally, I would revisit the architecture: