Is it possible to convert BroadcastReceiver to Coroutines? - android

I recently learned Coroutines and I am trying my best to implement it to everything.
I learned you could convert a callback to a coroutine.
Is it possible to convert a Broadcast Receiver to coroutines by using suspendCoroutine?
How do I do this?

Here is one way (courtesy of leonardkraemer and this answer):
suspend fun Context.getCurrentScanResults(): List<ScanResult> {
val wifiManager = getSystemService(Context.WIFI_SERVICE) as? WifiManager ?: return listOf()
return suspendCancellableCoroutine { continuation ->
val wifiScanReceiver = object : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) {
unregisterReceiver(this)
continuation.resume(wifiManager.scanResults)
}
}
}
continuation.invokeOnCancellation {
unregisterReceiver(wifiScanReceiver)
}
registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
wifiManager.startScan()
}
}

Related

How to test the reception of Broadcast Receivers?

I'm writing tests to verify the reception of the transmitting receivers but for some reason, the receiver is never registered or the intent is never sent.
I guess there should be a problem with the Context but, no luck yet finding it.
This is the BroadcastFactory.kt:
object BroadcastFactory {
private lateinit var intent: Intent
fun build(
action: String,
flag: Int? = null,
): BroadcastFactory {
intent = Intent().apply {
this.action = action
this.flags = flag ?: 0
}
return this
}
fun send(
context: Context
): Intent {
context.sendBroadcast(intent)
return intent
}
}
And this is the test file BroadcastTest.kt:
#RunWith(AndroidJUnit4::class)
#SmallTest
class BroadcastTest {
lateinit var intents: MutableList<Intent>
lateinit var latch: CountDownLatch
private lateinit var receiver: BroadcastReceiverTester
inner class BroadcastReceiverTester : BroadcastReceiver() {
override fun onReceive(p0: Context?, intent: Intent?) {
intent?.let {
intents.add(it)
latch.countDown()
}
}
}
private val context: Context = getInstrumentation().targetContext
#Before
fun setUp() {
intents = mutableListOf()
latch = CountDownLatch(1)
receiver = BroadcastReceiverTester()
LocalBroadcastManager.getInstance(context).registerReceiver(
receiver,
IntentFilter.create(
Constants.ACTION, "text/plain"
)
)
}
#Test
fun testBroadcastReception() {
BroadcastFactory
.build(Constants.ACTION, Constants.FLAG)
.send(context)
// assert broadcast reception (NOT WORKING)
latch.await(10, TimeUnit.SECONDS)
assertThat(intents.size).isEqualTo(1)
}
#After
fun tearDown() {
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
}
}
I'm using a CountDownLatch to wait 10 seconds for the receiver, plus, its value can be asserted. Besides, I set a list of Intents to check the number of registrations/receptions.
There is something I'm missing here? Different context provider? Robolectric runner?
Thanks
Is solved it by changing the receiver with this:
context.registerReceiver(
receiver,
IntentFilter(
Constants.ACTION
)
)
Thanks to #selvin and #mike-m for the help!

Wrap Broadcast receiver into Flow (coroutine)

I have a broadcast receiver for wifi scan results as a data source and I'd like to make it in coroutine way. I found an answer for suspend function here:
https://stackoverflow.com/a/53520496/5938671
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()
}
This is fine for signle emit, but if I want to get results while scanning is going then I'll get crash because cont.resume() could be called only once. Then I decided to try Flow. And here is my code:
suspend fun getCurrentScanResult(): Flow<List<ScanResult>> =
flow{
val wifiScanReceiver = object : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
if (intent.action?.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) == true) {
//context.unregisterReceiver(this)
emit(wifiManager.scanResults)
}
}
}
//setup cancellation action on the continuation
//register broadcast reciever
context.registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
//kick off scanning to eventually receive the broadcast
wifiManager.startScan()
}
But now Android Stuidio says Suspension functions can be called only within coroutine body for function emit(wifiManager.scanResults) Is there a way to use Flow here?
Please take a look at the callback flow which is specifically designed for this use case. Something like this will do the job:
callbackFlow {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) {
sendBlocking(wifiManager.scanResults) // or non-blocking offer()
}
}
}
context.registerReceiver(receiver, intentFilter)
awaitClose {
context.unregisterReceiver(receiver)
}
}
You also might want to share this flow with e.g. shareIn operator to avoid registering a new receiver for each flow subscriber.

How to send context to BroadcastReceiver?

I want to use my BroadcastReceiver as sender of data into my activity. For this reason I'm using LocalBroadcastManager. This manager is used to register and unregister my receiver. Problem is that Context in onReceive method is different than Context in onStart and onStop method.
I need to pass activity context into my BroadcastReceiver or instance of LocalBroadcastManager initialized inside Activity. Because my receiver is not receiving any data.
Maybe it is not fault of this manager context but I don't know why it doesnt work since I implemented this manager.
class GPSReceiver: BroadcastReceiver(){
companion object{
const val GPS_PAYLOAD = "gps_payload"
}
override fun onReceive(context: Context, intent: Intent) {
try {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val int = Intent(GPS_PAYLOAD)
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
int.putExtra(GPS_PAYLOAD, true)
} else {
int.putExtra(GPS_PAYLOAD, false)
}
LocalBroadcastManager.getInstance(context).sendBroadcast(int)
} catch (ex: Exception) {
}
}
}
Registering receiver inside Activity:
private val gpsStatusReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
App.log("isGpsEnabled: onReceive")
val gpsStatus = intent?.extras?.getBoolean(GPS_PAYLOAD)
if (gpsStatus != null) {
if (gpsStatus){
App.log("isGpsEnabled: true")
hideGpsSnackbar()
} else {
App.log("isGpsEnabled: false")
showGpsSnackbar()
}
} else {
App.log("isGpsEnabled: null")
}
}
}
override fun onStart() {
super.onStart()
LocalBroadcastManager.getInstance(this).apply {
val filter = IntentFilter()
filter.apply {
addAction("android.location.PROVIDERS_CHANGED")
addAction(GPS_PAYLOAD)
}
registerReceiver(gpsStatusReceiver, filter)
}
}
I have seen your code. So there is not issue with context, but in the approach.
Your are registering your reciever with the same strings in which you are getting you data inside the Reciever.
So Send Your broadcast from Fragment/Activity
Send BroadCast Like
private fun sendSuccessfulCheckoutEvent() {
val intent = Intent("successful_checkout_event")
intent.putExtra("cartID", cartId)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
And Listen it in Activity/Fragment like this
1) Create broadcast Reciever
private val checkoutDoneReciever : BroadcastReceiver = object : BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val cartNumbers = intent.getIntExtra("cartID", 0)
Log.d("receiver", "Got message: $cartNumbers.toString()")
}
}
2) Register it in onCreate()/onStart()
LocalBroadcastManager.getInstance(this).registerReceiver(cartUpdatedReceiver,IntentFilter("successful_checkout_event"))
3) Unregister it in onDestroy()
LocalBroadcastManager.getInstance(this).unregisterReceiver(cartUpdatedReceiver)

Broadcast Receiver receive information only once

I have a service which sends two broadcasts at the same time.
val i = Intent(PlayerService.INTENT_ACTION)
i.putExtra(EVENT_EXTRAS, PlayerEvent.PLAYER_READY.ordinal)
i.putExtra(DURATION_EXTRAS, mp.duration) //some duration
sendBroadcast(i)
val i1 = Intent(PlayerService.INTENT_ACTION)
i1.putExtra(EVENT_EXTRAS, PlayerEvent.ON_SECOND_CHANGED.ordinal)
i1.putExtra(DURATION_EXTRAS, player.duration) //another duration
sendBroadcast(i1)
The action of intents is the same, but the extras is different. Finally, I only get the answer from the second broadcast. Who knows what the cause is?
My Receiver Live Data:
class PlayerLiveEvent(val context: Context) : LiveData<Intent>() {
override fun onActive() {
super.onActive()
context.registerReceiver(receiver, IntentFilter(PlayerService.INTENT_ACTION))
}
override fun onInactive() {
super.onInactive()
context.unregisterReceiver(receiver)
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
postValue(intent)
}
}
}
Fragment where I observe these events:
PlayerLiveEvent(activity!!).observe(this, Observer {
it?.apply {
val event = PlayerEvent.values()[getIntExtra(EVENT_EXTRAS, 0)]
when (event) {
PlayerEvent.PLAYER_READY -> {
println("PLAYER_READY")
}
PlayerEvent.ON_SECOND_CHANGED -> {
println("ON_SECOND_CHANGED")
}
else -> println()
}
}
})
Your second onReceive is called before a postValue task from the first onReceive is executed on the main thread and hence the value set the second time is ignored. You can also see this from the implementation of postValue:
...
synchronized (mDataLock) {
// for your second call this will be false as there's a pending value
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
// so this is true and so the method returns prematurely
if (!postTask) {
return;
}
...
Thereof, use setValue because it sets the value immediately and is called from the main thread.
The problem is in LiveData, the events are not being transmitted as needed.
This document explains why postValue posts only once. That is why the solution would be the following:
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
value = intent
}
}

Problems with JobScheduler - JobsService firing multiple times

I'm trying to do some background work in my android application. As the web suggested I'm using JobScheduler to do so.
The jobs are sometimes firing 5-15 times instead of once. Sometimes they are never firing.
My testdevices run on 5.1.1 and 7.0. The one with Nougat fires way less then the one with lollipop.
This is how I enable my jobs (the 5 seconds interval is only for test purpose):
fun enableTasks() {
val jobScheduler = App.getContext().getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
if (PreferenceDao.getInstance().shouldUpdateJob()) jobScheduler.cancelAll()
scheduleJob(jobScheduler, MoniInfoJob.getJob())
scheduleJob(jobScheduler, QueueJob.getJob())
scheduleJob(jobScheduler, MontageOrderUpdateJob.getJob())
PreferenceDao.getInstance().setJobUpdated()
}
private fun scheduleJob(jobScheduler: JobScheduler, jobInfo: JobInfo) {
val jobExists = jobScheduler.allPendingJobs.any { it.id == jobInfo.id }
if (!jobExists) jobScheduler.schedule(jobInfo)
}
All three jobs look kind of the same so I only post one:
The JobService
class QueueJob : JobService() {
override fun onStartJob(jobParameters: JobParameters?): Boolean {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, p1: Intent?) {
unregisterBroadcastReceiver(this)
jobFinished(jobParameters, false)
}
}
registerBroadcastReceiver(receiver)
MainController.startQueueService()
return true;
}
override fun onStopJob(jobParameters: JobParameters): Boolean {
Log.d(MontageOrderUpdateJob.TAG, "onStopJob")
return false;
}
private fun registerBroadcastReceiver(receiver: BroadcastReceiver) {
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, IntentFilter(JOB_FINISHED))
}
private fun unregisterBroadcastReceiver(receiver: BroadcastReceiver) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
}
companion object {
val TAG = QueueJob::class.java.name
val jobId: Int = 2
val JOB_FINISHED = TAG + "_finished"
fun getJob(): JobInfo {
val builder = JobInfo.Builder(jobId, ComponentName(App.getContext(), TAG))
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
builder.setPeriodic(5000L)
builder.setPersisted(true)
return builder.build()
}
}
}
And the JobIntentService:
class QueueService : JobIntentService() {
private val TAG = QueueService::class.java.name
override fun onHandleWork(intent: Intent) {
try {
Log.d(TAG, "Jobservice started")
TimerecordQueue().workThroughQueue()
DangerAllowanceQueue().workThroughQueue()
ProjektEndQueue().workThroughQueue()
PhotoUploadQueue().workThroughQueue()
} finally {
sendFinishedBroadcast()
}
}
private fun sendFinishedBroadcast() {
val jobFinishedIntent = Intent(QueueJob.JOB_FINISHED)
LocalBroadcastManager.getInstance(this).sendBroadcast(jobFinishedIntent)
}
}
I had a similar problem once. My problem then was that I didn't check for preexisting schedules.
Could it be you need to do the same?

Categories

Resources