Skip to content

Instantly share code, notes, and snippets.

@psteiger
Created September 28, 2019 17:08
Show Gist options
  • Select an option

  • Save psteiger/f3c359242f63c9fe46a74963ea5fa0ca to your computer and use it in GitHub Desktop.

Select an option

Save psteiger/f3c359242f63c9fe46a74963ea5fa0ca to your computer and use it in GitHub Desktop.
package com.faztudo.common.livedata
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import com.faztudo.App
import com.faztudo.INearbyUsersUseCases
import com.faztudo.common.helpers.logd
import com.faztudo.common.helpers.rootRef
import com.faztudo.common.helpers.sortedByDistance
import com.faztudo.common.helpers.usersRef
import com.faztudo.common.models.User
import com.firebase.geofire.GeoFire
import com.firebase.geofire.GeoLocation
import com.firebase.geofire.GeoQuery
import com.firebase.geofire.GeoQueryEventListener
import com.google.android.gms.maps.model.LatLng
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class NearbyUsersLiveData : LiveData<Resource<List<User>>>(),
GeoQueryEventListener,
INearbyUsersUseCases,
CoroutineScope by CoroutineScope(Dispatchers.Default) {
companion object {
private lateinit var sInstance: NearbyUsersLiveData
@MainThread
fun get(): NearbyUsersLiveData {
sInstance = if (Companion::sInstance.isInitialized) sInstance else NearbyUsersLiveData()
logd("NearbyUsersLiveData", "$this observers = ${sInstance.hasObservers()}")
return sInstance
}
}
init {
value = Resource.Loading()
}
sealed class Msg {
class AddKey(
val key: String,
val location: GeoLocation
): Msg()
class RemoveKey(val key: String): Msg()
object GeoQueryReady: Msg() // for removal optimization
object Active: Msg()
object Inactive: Msg()
class SetLocationForUser(
val uid: String,
val geoCenter: GeoLocation,
val retry: Boolean = true,
val callback: (() -> Unit)? = null
): Msg()
class SetRadius(val r: Double): Msg()
object GetQueryCenter: Msg()
class OnUserDataChange(
val snap: DataSnapshot,
val key: String,
val l: GeoLocation
): Msg()
object OnUserDataChangeCancelled: Msg()
class QueryAtLocation(
val l: GeoLocation,
val r: Double
): Msg()
}
val myActor = actor<Msg>(coroutineContext, Channel.UNLIMITED) {
var pending = 0
//val users = sortedMapOf<String, User>()
var geoQuery: GeoQuery? = null
val geoFire = GeoFire(rootRef.child("geofire"))
val userChangeListeners = mutableMapOf<String, ValueEventListener>()
val currentTime = System.currentTimeMillis()
val usersMap = mutableMapOf<String, User>()
fun onKeyEntered(key: String, location: GeoLocation) {
// logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} onKeyEntered $key")
if (!userChangeListeners.containsKey(key)) {
++pending
val eventListener = UserValueEventListener(key, location)
usersRef.child(key).addValueEventListener(eventListener)
userChangeListeners[key] = eventListener
}
}
fun onKeyExited(key: String) {
// logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} onKeyExited $key")
userChangeListeners.remove(key)?.let {
usersRef.removeEventListener(it)
usersMap.remove(key)
}
}
fun geoQueryReady() {
logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} geoQueryReady")
//if (users.value != value)
postValue(Resource.Success(usersMap.values.toList().sortedByDistance()))
}
fun activate() {
logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} geoQuery live data active")
try {
geoQuery?.addGeoQueryEventListener(this@NearbyUsersLiveData)
} catch (_: IllegalArgumentException) {
}// https://console.firebase.google.com/u/0/project/faztudo-5b6ae/crashlytics/app/android:com.faztudo/issues/58dd3597de18bd1966cc23f75b34400c?time=last-seven-days&sessionId=5D237F9D015000027D3BB431AE20BA59_DNE_0_v2
pending = userChangeListeners.size
userChangeListeners.forEach { (key, eventListener) ->
usersRef.child(key).addValueEventListener(eventListener)
}
}
fun inactivate() {
logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} geoQuery live data inactive")
pending = 0 // necessary?
geoQuery?.removeGeoQueryEventListener(this@NearbyUsersLiveData)
userChangeListeners.forEach { (key, eventListener) ->
usersRef.child(key).removeEventListener(eventListener)
}
}
fun setLocationForUser(
uid: String,
geoCenter: GeoLocation,
retry: Boolean = true,
callback: (() -> Unit)? = null
) {
logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} setting location for user $uid")
geoFire.setLocation(uid, geoCenter) { _, error ->
if (error != null) {
if (retry) {
geoFire.setLocation(uid, geoCenter) { _, _ ->
callback?.invoke()
}
} else {
callback?.invoke()
}
} else {
callback?.invoke()
}
}
}
fun setRadius(r: Double) {
postValue(Resource.Loading(usersMap.values.toList().sortedByDistance()))
logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} setting radius $r")
geoQuery?.radius = r
}
fun getQueryCenter(): GeoLocation? =
geoQuery?.center
fun onUserDataChange(snap: DataSnapshot, key: String, l: GeoLocation) {
snap.getValue(User::class.java)?.run {
// unacceptable conditions
if (lastOnline == null || currentTime - lastOnline as Long > App.MONTH_IN_MILLISECONDS) {
geoFire.removeLocation(key)
return@run
}
if (name.isNullOrBlank() || uid.isNullOrBlank())
return@run
position = LatLng(l.latitude, l.longitude)
usersMap[key] = this
}
// update only when done
--pending
// "".logd("pending = $pending, snap = $snap")
if (pending <= 0) {
pending = 0
postValue(Resource.Success(usersMap.values.toList().sortedByDistance()))
}
}
fun onUserDataChangeCancelled() {
--pending
if (pending <= 0) {
pending = 0
postValue(Resource.Success(usersMap.values.toList().sortedByDistance()))
}
}
fun queryAtLocation(l: GeoLocation, r: Double) {
logd("NearbyUsersLiveData", "$this ${this@NearbyUsersLiveData} queryAtLocation")
postValue(Resource.Loading(usersMap.values.toList()))
geoQuery = geoFire.queryAtLocation(l, r).apply {
addGeoQueryEventListener(this@NearbyUsersLiveData)
}
// "".logd("querying at $l radius $r")
}
for (msg in channel) when (msg) {
is Msg.AddKey -> onKeyEntered(msg.key, msg.location)
is Msg.RemoveKey -> onKeyExited(msg.key)
Msg.GeoQueryReady -> geoQueryReady()
Msg.Active -> activate()
Msg.Inactive -> inactivate()
is Msg.SetLocationForUser -> setLocationForUser(msg.uid, msg.geoCenter, msg.retry, msg.callback)
is Msg.SetRadius -> setRadius(msg.r)
Msg.GetQueryCenter -> getQueryCenter()
is Msg.OnUserDataChange -> onUserDataChange(msg.snap, msg.key, msg.l)
Msg.OnUserDataChangeCancelled -> onUserDataChangeCancelled()
is Msg.QueryAtLocation -> queryAtLocation(msg.l, msg.r)
}
}
override fun queryAtLocation(l: GeoLocation, r: Double) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.QueryAtLocation(l, r)) }
}
override fun setRadius(r: Double) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.SetRadius(r)) }
}
override fun setLocationForUser(
uid: String,
geoCenter: GeoLocation,
retry: Boolean,
callback: (() -> Unit)?
) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.SetLocationForUser(uid, geoCenter, retry, callback)) }
}
// fun getQueryCenter(): GeoLocation? =
// launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.GetQueryCenter) }
override fun onGeoQueryError(error: DatabaseError?) = Unit
override fun onGeoQueryReady() {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.GeoQueryReady) }
}
override fun onKeyEntered(key: String, location: GeoLocation) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.AddKey(key, location)) }
}
override fun onKeyExited(key: String) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.RemoveKey(key)) }
}
override fun onKeyMoved(key: String?, location: GeoLocation?) = Unit
override fun onActive() {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.Active) }
}
override fun onInactive() {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.Inactive) }
}
inner class UserValueEventListener(private val key: String, private val l: GeoLocation) : ValueEventListener {
override fun onDataChange(snap: DataSnapshot) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.OnUserDataChange(snap, key, l)) }
}
override fun onCancelled(p0: DatabaseError) {
launch(start = CoroutineStart.UNDISPATCHED) { myActor.send(Msg.OnUserDataChangeCancelled) }
}
}
//override fun getAll() = this
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment