如何在Android中从Boradcast Receiver调用服务的方法?

问题描述 投票:0回答:1

我为我的下载 TrackDownloadService 实现了同步下载机制。当我从通知中单击“取消”时,我无法触发 cancelDownload() 方法来取消下载。如何让它发挥作用?如果我的代码做错了什么,请给我一些建议。

这是 PausingDispatchQueue,负责暂停和恢复功能:

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>
}

这是 TracksDownloadService 类:

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
    }
}

这是用于接收取消意图的 DonwloadCancelReceiver 类

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()
        }
    }
}

这里,接收者成功获取了 Intent 操作,但 CancelDownload() 方法没有从 DownloadCancelReceiver 调用到 TrackDownloadService,最终取消不起作用。

android kotlin service broadcastreceiver android-pendingintent
1个回答
0
投票

TrackDownloadService.getInstance()
始终会返回
null
,因为
instance
始终是
null

我建议删除

instance
getInstance()
。使用您当前的架构,您可以通过构造函数参数传入
TrackDownloadService

class DownloadCancelReceiver(service: TrackDownloadService) : BroadcastReceiver() {
    // rest of code goes here
}
cancelReceiver = DownloadCancelReceiver(this)

就我个人而言,我会重新审视架构:

  • 将实际的下载逻辑移至单例存储库,以便于测试
  • 使用您首选的依赖倒置框架(Dagger/Hilt、Koin 等)将该存储库注入到服务(可能还有接收器)中
  • 让取消操作成为存储库上的一个函数,接收者可以调用该函数
  • 让服务管理其前台通知,但将工作委托给存储库
© www.soinside.com 2019 - 2024. All rights reserved.