Created
September 28, 2019 17:08
-
-
Save psteiger/f3c359242f63c9fe46a74963ea5fa0ca to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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