AltBeacon / android-beacon-library

Allows Android apps to interact with BLE beacons
Apache License 2.0
2.83k stars 834 forks source link

LiveData sends last value #1103

Closed devasidmi closed 5 months ago

devasidmi commented 1 year ago

Ranging feature uses LiveData which sends old value on first scan when I'm making new observer, do u have any workarounds? Thanks

davidgyoung commented 1 year ago

Two possibilities:

  1. you could add code to your observer to ignore the first value
  2. You could switch to using the RangeNotifier interface and receive callbacks on each range event.

I do understand this is awkward with LiveData observers. I am open to better ideas.

brendandodd commented 10 months ago

+1 for this, same issue with RangeNotifier also. Currently using a workaround to always discard the first value

davidgyoung commented 10 months ago

I was reading this SO post regarding how other folks have solved the more general problem of ignoring stale info with LiveData. Clearly there are lots of complicating factors that make the right solution dependent on your specific use case of using the data.

I am reluctant to add something to fix one use case that may cause trouble with other use cases we are not thinking about.

A couple of thoughts to summarize the problem:

  1. This is really only a problem with RegionViewModel.rangedBeacons not with RegionViewModel.regionState
  2. The main problem with RegionViewModel.rangedBeacons returning a stale value comes up when the list of rangedBeacons is not empty -- one of those beacons may have been detected many hours or days ago.

If my problem statement above is correct, then it seems the existing API can solve this with a single if statement that checks the age of the beacon detection like this:

    val rangingObserver = Observer<Collection<Beacon>> { beacons ->
        val rangeAgeMillis = System.currentTimeMillis() - (beacons.firstOrNull()?.lastCycleDetectionTimestamp ?:  System.currentTimeMillis())
        if (rangeAgeMillis < 10000) {
            Log.d(MainActivity.TAG, "Ranged: ${beacons.count()} beacons")
        }
        else {
            Log.d(MainActivity.TAG, "Ignoring stale ranged beacons from $rangeAgeMillis millis ago")
        }
    }

If zero beacons are detected, then the stale ranging data would still be processed (as no timestamp is known) but it seems like that is a minimal problem, because if you expect to be ranging, a new update should come in shortly. And it is not uncommon when ranging for there to be temporary dropouts in detections where you see no beacons, so most code will handle this situation already.

If this solution is workable, it might be possible to add some helper code to eliminate the boilerplate around the calculation of val rangeAgeMillis = System.currentTimeMillis() - (beacons.firstOrNull()?.lastCycleDetectionTimestamp ?: System.currentTimeMillis())

Many of the "proper" LiveData solutions offered in the referenced SO posts above would require some other if statement in the observer to handle this kind of situation, anyway. So it seems reasonable to use an if statement associated with already existing functionality inside the API.

Thoughts?

brendandodd commented 10 months ago

Sure that works, thanks for your help.

Here's what I ended up with, still using a RangeNotifier as the beacons collection suffered from the same stale data as LiveData + observer.

For my application I'm running singular, long scans in the foreground, so I've tied the rangeAge to the foregroundScanPeriod.


beaconManager.addRangeNotifier(object : RangeNotifier {
                override fun didRangeBeaconsInRegion(beacons: Collection<Beacon>, region: Region) {
                    val rangeAgeMillis = System.currentTimeMillis() - (beacons.firstOrNull()?.lastCycleDetectionTimestamp ?:  System.currentTimeMillis())
                    if (rangeAgeMillis < beaconManager.foregroundScanPeriod) {
                        val nearestBeacon = beacons.minByOrNull { it.distance }
                        processBeacon(nearestBeacon)

                        return
                    }
                    else {
                        Log.d(TAG, "Ignoring stale ranged beacons from $rangeAgeMillis millis ago")
                    }
                }
            })
davidgyoung commented 10 months ago

Glad to hear this worked for you, @brendandodd.

I'm not sure it matters to you, but I suspect any stale data you see with the RangeNotifier has a different cause than what you see with the RegionViewModel.rangedBeacons LiveData model. While LiveData will always hold on to the last known value and will deliver it immediately to newly registered observers, the RangeNotifier won't do that. The stale data you see with RangeNotifier may be caused by your app doing long scans in the foreground, but the UI being blocked from executing when not visible on the screen. This can sometimes cause stale data to be queued for delivery, but not processed by UI elements until the app returns to the foreground.

@devasidmi any thoughts on this solution?

davidgyoung commented 5 months ago

closed due to inactivity