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
}
}
Related
I will send step count:
Foreground Service -> Broadcast Receiver -> Fragment
Since I need to keep tracking step count on background and when app is off, I've created two Broadcast Receiver, one for notification to keep showing step count on background and one for fragment UI which will be unregistered on Destroy.
So this process is for the latter.
Foreground Service
: I send "stepCount" value to broadcast.
Intent().also { intent ->
intent.setAction(ACTION_STEP_COUNTER_NOTIFICATION)
intent.putExtra("stepCount", "$todayTotalStepCount")
sendBroadcast(intent)
}
BroadcastReceiver
open class StepCountBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
if(intent!!.action == ACTION_STEP_COUNTER_NOTIFICATION) {
var intent = Intent()
var stepCount = intent.getStringExtra("stepCount")
var sendMyDiary = Intent(context!!, MyDiaryFragment::class.java)
sendMyDiary.putExtra("stepCount", stepCount)
context.startActivity(sendMyDiary)
}
}
}
when custom Action is triggered, this will get 'stepCount' from Service and here I will send it to Fragment named MyDiaryFragment.
So I send it by using Intent().putExtra.
Fragment
private val stepCountBroadcastReceiver: BroadcastReceiver = StepCountBroadcastReceiver()
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
addAction(ACTION_STEP_COUNTER_NOTIFICATION)
}
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(stepCountBroadcastReceiver, filter)
Here is my question.
I register this StepCountBroadcastReceiver in onCreate.
But I don't know how to get stepCount coming from broadcast in Fragment whenever Broadcastreceiver is called.
It that same process like just adding this below line at the bottom?
var stepCount = requireActivity().intent.getStringExtra("stepCount")
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
addAction(ACTION_STEP_COUNTER_NOTIFICATION)
}
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(stepCountBroadcastReceiver, filter)
var stepCount = requireActivity().intent.getStringExtra("stepCount")
In your broadcast receiver, add this.To Send a LocalBroadCast
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
if(intent?.action == ACTION_STEP_COUNTER_NOTIFICATION) {
var stepCount = intent.getStringExtra("stepCount")
var localBroadCastIntent = Intent(LOCAL_BROADCAST_KEY)
localBroadCastIntent.putExtra("stepCount",stepCount?:"")
Handler(Looper.getMainLooper()).post {
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
}
}
const val LOCAL_BROADCAST_KEY = "step_counter_local_broadcast"
In your Fragment Call this register function.And in onCreateView and make sure to call unregisterReceiver in ondestroyview of Fragment.
private fun registerBroadCastReceiver() {
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(
receiver,
IntentFilter(LOCAL_BROADCAST_KEY)
)
}
}
Add this BroadCast Receiver,in Fragment Class globally.
private var receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val stepCount = it.getStringExtra("stepCount")
}
}
}
Note:
Here Flow will be like this.
Service -> BroadCast Receiver -> LocalBroadCastReceiver ->
UpdateFragment UI.
You can also directly call this localbroadcast from service based on your usecase. So it will be like below.
Service-> LocalBroadCastReceiver -> UpdateFragment UI.
So only when fragment is alive, the local broadcast receiver will receive messages and update ui.
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!
I want to use MutableSharedFlow in the Service class, but I'm not sure how to stop subscribing when Service ends. How to implement the MutableSharedFlow function in service or any other function available to listen to stream data?
To use a Flow in an android Service class we need a CoroutineScope instance to handle launching coroutines and cancellations. Please see the following code with my comments:
class CoroutineService : Service() {
private val scope = CoroutineScope(Dispatchers.IO)
private val flow = MutableSharedFlow<String>(extraBufferCapacity = 64)
override fun onCreate() {
super.onCreate()
// collect data emitted by the Flow
flow.onEach {
// Handle data
}.launchIn(scope)
}
override fun onStartCommand(#Nullable intent: Intent?, flags: Int, startId: Int): Int {
scope.launch {
// retrieve data from Intent and send it to Flow
val messageFromIntent = intent?.let { it.extras?.getString("KEY_MESSAGE")} ?: ""
flow.emit(messageFromIntent)
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
scope.cancel() // cancel CoroutineScope and all launched coroutines
}
}
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.
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)