Coroutine unregister reciever on cancel

2019-07-31 21:11发布

问题:

My coroutine leaks a broadcast receiver, when the service is stopped. This is because the service stops, before the callback is finished. How can I cancel coroutine in a way that lets me unregister the reciever?

The Service works like this:

class DataCollectorService : Service(){
    var job : Job? = null

    override fun onStartCommand(...){
        job = GlobalScope.launch {
            val location = async { wifiScanner.getCurrentLocation() }
            //other asynchronous jobs
            location.await() 
            //do something with location
        }
    }

    override fun onDestroy(){
         job?.cancel()
    }
}

Here is the class where the BroadcastReciever is not unregistered properly in onDestroy:

class WifiScanner(val context: ContextWrapper) {
    val wifiManager: WifiManager

    init {
        wifiManager = context.baseContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
    }

    suspend fun getCurrentScanResult(): List<ScanResult> =
        suspendCoroutine { cont ->
            val wifiScanReceiver = object : BroadcastReceiver() {
                override fun onReceive(c: Context, intent: Intent) {
                    if (intent.action?.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) == true) {
                        context.unregisterReceiver(this)
                        cont.resume(wifiManager.scanResults)
                    }
                }
            }
            context.registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
            wifiManager.startScan()
        }
}

Example stacktrace:

de.leo.smartTrigger E/ActivityThread: Service de.leo.smartTrigger.datacollector.datacollection.DataCollectorService has leaked IntentReceiver de.leo.smartTrigger.datacollector.datacollection.sensors.WifiScanner$getCurrentScanResult$$inlined$suspendCoroutine$lambda$1@6febc2f that was originally registered here. Are you missing a call to unregisterReceiver()?
        android.app.IntentReceiverLeaked: Service de.leo.smartTrigger.datacollector.datacollection.DataCollectorService has leaked IntentReceiver de.leo.smartTrigger.datacollector.datacollection.sensors.WifiScanner$getCurrentScanResult$$inlined$suspendCoroutine$lambda$1@6febc2f that was originally registered here. Are you missing a call to unregisterReceiver()?
            at android.app.LoadedApk$ReceiverDispatcher.<init>(LoadedApk.java:1355)
            at android.app.LoadedApk.getReceiverDispatcher(LoadedApk.java:1120)
            at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1428)
            at android.app.ContextImpl.registerReceiver(ContextImpl.java:1401)
            at android.app.ContextImpl.registerReceiver(ContextImpl.java:1389)
            at android.content.ContextWrapper.registerReceiver(ContextWrapper.java:622)
            at de.leo.smartTrigger.datacollector.datacollection.sensors.WifiScanner.getCurrentScanResult(WifiScanner.kt:35)
            at de.leo.smartTrigger.datacollector.datacollection.DataCollectorService.getWifi(DataCollectorService.kt:219)
            at de.leo.smartTrigger.datacollector.datacollection.DataCollectorService$uploadDataSet$1$wifi$1.invokeSuspend(DataCollectorService.kt:273)
            at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
            at kotlinx.coroutines.DispatchedTask$DefaultImpls.run(Dispatched.kt:221)
            at kotlinx.coroutines.DispatchedContinuation.run(Dispatched.kt:67)
            at ...

回答1:

The answer was indeed to use suspendCancellableCoroutine and define cont.invokeOnCancellation as written below:

suspend fun getCurrentScanResult(): List<ScanResult> =
        suspendCancellableCoroutine { cont ->
            //define broadcast reciever
            val wifiScanReceiver = object : BroadcastReceiver() {
                override fun onReceive(c: Context, intent: Intent) {
                    if (intent.action?.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) == true) {
                        context.unregisterReceiver(this)
                        cont.resume(wifiManager.scanResults)
                    }
                }
            }
            //setup cancellation action on the continuation
            cont.invokeOnCancellation {
                context.unregisterReceiver(wifiScanReceiver)
            }
            //register broadcast reciever
            context.registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
            //kick off scanning to eventually receive the broadcast
            wifiManager.startScan()
        }