mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-10-25 07:40:36 +00:00
521 lines
23 KiB
Kotlin
521 lines
23 KiB
Kotlin
package com.bitchat.android.ui
|
|
|
|
import android.app.Application
|
|
import android.util.Log
|
|
import androidx.lifecycle.AndroidViewModel
|
|
import androidx.lifecycle.DefaultLifecycleObserver
|
|
import androidx.lifecycle.Lifecycle
|
|
import androidx.lifecycle.LifecycleOwner
|
|
import androidx.lifecycle.ProcessLifecycleOwner
|
|
import androidx.lifecycle.viewModelScope
|
|
import com.bitchat.android.geohash.GeohashNostrPrivacyPolicy
|
|
import com.bitchat.android.geohash.LiveLocationPrivacyGate
|
|
import com.bitchat.android.nostr.GeohashMessageHandler
|
|
import com.bitchat.android.nostr.GeohashRepository
|
|
import com.bitchat.android.nostr.NostrBackgroundRuntime
|
|
import com.bitchat.android.nostr.NostrIdentityBridge
|
|
import com.bitchat.android.nostr.NostrProtocol
|
|
import com.bitchat.android.nostr.NostrRelayManager
|
|
import com.bitchat.android.nostr.NostrSubscriptionManager
|
|
import com.bitchat.android.nostr.PoWPreferenceManager
|
|
import com.bitchat.android.nostr.GeohashAliasRegistry
|
|
import com.bitchat.android.nostr.GeohashConversationRegistry
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.launch
|
|
import java.util.Date
|
|
import kotlinx.coroutines.isActive
|
|
import java.util.UUID
|
|
|
|
class GeohashViewModel(
|
|
application: Application,
|
|
private val state: ChatState,
|
|
private val messageManager: MessageManager,
|
|
private val dataManager: DataManager,
|
|
private val notificationManager: NotificationManager
|
|
) : AndroidViewModel(application), DefaultLifecycleObserver {
|
|
|
|
companion object { private const val TAG = "GeohashViewModel" }
|
|
|
|
private val repo = GeohashRepository(application, state, dataManager)
|
|
private val uiSubscriptionOwner = "geohash-ui-${UUID.randomUUID()}"
|
|
private val subscriptionManager = NostrSubscriptionManager(
|
|
application,
|
|
owner = uiSubscriptionOwner
|
|
)
|
|
private val geohashMessageHandler = GeohashMessageHandler(
|
|
application = application,
|
|
repo = repo,
|
|
scope = viewModelScope,
|
|
dataManager = dataManager,
|
|
addChannelMessage = messageManager::addChannelMessage
|
|
)
|
|
|
|
// Presence heartbeat firehose (kind 20001). High-volume; paused while backgrounded.
|
|
private var currentGeohashPresenceSubId: String? = null
|
|
private var geoTimer: Job? = null
|
|
private var locationChannelManager: com.bitchat.android.geohash.LocationChannelManager? = null
|
|
private val activeSamplingGeohashes = mutableSetOf<String>()
|
|
private val samplingSubscriptionIds = mutableMapOf<String, String>()
|
|
private val liveSamplingSubscriptionGeohashes = mutableSetOf<String>()
|
|
private var requestedLiveSamplingGeohashes: Set<String> = emptySet()
|
|
private var requestedUserSamplingGeohashes: Set<String> = emptySet()
|
|
private var uiSubscriptionsShutdown = false
|
|
private val liveLocationRevocationListener: () -> Unit = {
|
|
val revokedLiveGeohashes = liveSamplingSubscriptionGeohashes.toSet()
|
|
revokedLiveGeohashes.forEach { geohash ->
|
|
samplingSubscriptionIds.remove(geohash)
|
|
activeSamplingGeohashes.remove(geohash)
|
|
}
|
|
liveSamplingSubscriptionGeohashes.clear()
|
|
requestedLiveSamplingGeohashes = emptySet()
|
|
}
|
|
|
|
// Geohash of the currently selected Location channel (null for Mesh/none).
|
|
private var activeChannelGeohash: String? = null
|
|
|
|
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
|
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
|
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
|
|
|
init {
|
|
LiveLocationPrivacyGate.addRevocationListener(liveLocationRevocationListener)
|
|
}
|
|
|
|
fun initialize() {
|
|
// Observe process lifecycle to manage background sampling
|
|
kotlin.runCatching {
|
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
}
|
|
try {
|
|
locationChannelManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
|
|
viewModelScope.launch {
|
|
locationChannelManager?.selectedChannel?.collect { channel ->
|
|
state.setSelectedLocationChannel(channel)
|
|
switchLocationChannel(channel)
|
|
}
|
|
}
|
|
viewModelScope.launch {
|
|
locationChannelManager?.teleported?.collect { teleported ->
|
|
state.setIsTeleported(teleported)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to initialize location channel state: ${e.message}")
|
|
state.setSelectedLocationChannel(com.bitchat.android.geohash.ChannelID.Mesh)
|
|
state.setIsTeleported(false)
|
|
}
|
|
}
|
|
|
|
fun panicReset() {
|
|
repo.clearAll()
|
|
GeohashAliasRegistry.clear()
|
|
GeohashConversationRegistry.clear()
|
|
subscriptionManager.unsubscribeAllOwned()
|
|
currentGeohashPresenceSubId = null
|
|
activeChannelGeohash = null
|
|
geoTimer?.cancel()
|
|
geoTimer = null
|
|
try { NostrIdentityBridge.clearAllAssociations(getApplication()) } catch (_: Exception) {}
|
|
NostrBackgroundRuntime.resetSubscriptions()
|
|
}
|
|
|
|
fun sendGeohashMessage(content: String, channel: com.bitchat.android.geohash.GeohashChannel, myPeerID: String, nickname: String?) {
|
|
viewModelScope.launch {
|
|
try {
|
|
val canUseChannel = locationChannelManager
|
|
?.canUseSelectedLocationChannel(channel) == true
|
|
if (!canUseChannel) {
|
|
Log.w(TAG, "Blocked message to a stale live-location channel")
|
|
return@launch
|
|
}
|
|
val isLiveDerived = locationChannelManager
|
|
?.isSelectedChannelLiveDerived(channel) == true
|
|
val liveLocationToken = locationChannelManager
|
|
?.liveLocationTokenForSelectedChannel(channel)
|
|
if (isLiveDerived && liveLocationToken == null) return@launch
|
|
val tempId = "temp_${System.currentTimeMillis()}_${kotlin.random.Random.nextInt(1000)}"
|
|
val pow = PoWPreferenceManager.getCurrentSettings()
|
|
val localMsg = com.bitchat.android.model.BitchatMessage(
|
|
id = tempId,
|
|
sender = nickname ?: myPeerID,
|
|
content = content,
|
|
timestamp = Date(),
|
|
isRelay = false,
|
|
senderPeerID = "geohash:${channel.geohash}",
|
|
channel = "#${channel.geohash}",
|
|
powDifficulty = if (pow.enabled) pow.difficulty else null
|
|
)
|
|
messageManager.addChannelMessage("geo:${channel.geohash}", localMsg)
|
|
val identity = NostrIdentityBridge.deriveIdentity(
|
|
forGeohash = channel.geohash,
|
|
context = getApplication()
|
|
)
|
|
val teleported = locationChannelManager?.teleported?.value
|
|
?: state.isTeleported.value
|
|
val event = NostrProtocol.createEphemeralGeohashEvent(
|
|
content,
|
|
channel.geohash,
|
|
identity,
|
|
nickname,
|
|
teleported
|
|
)
|
|
val relayManager = NostrRelayManager.getInstance(getApplication())
|
|
relayManager.sendEventToGeohash(
|
|
event,
|
|
channel.geohash,
|
|
includeDefaults = false,
|
|
nRelays = 5,
|
|
liveLocationToken = liveLocationToken
|
|
)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to send geohash message: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun beginGeohashSampling(
|
|
liveLocationGeohashes: Collection<String>,
|
|
userSelectedGeohashes: Collection<String>,
|
|
) {
|
|
requestedLiveSamplingGeohashes = liveLocationGeohashes.toSet()
|
|
requestedUserSamplingGeohashes = userSelectedGeohashes.toSet()
|
|
reconcileSamplingSubscriptions()
|
|
}
|
|
|
|
private fun reconcileSamplingSubscriptions() {
|
|
val currentSet = activeSamplingGeohashes.toSet()
|
|
val newSet = GeohashNostrPrivacyPolicy.samplingTargets(
|
|
liveLocationGeohashes = requestedLiveSamplingGeohashes,
|
|
userSelectedGeohashes = requestedUserSamplingGeohashes,
|
|
liveLocationEnabled = LiveLocationPrivacyGate.isEnabled
|
|
)
|
|
|
|
val toRemove = currentSet - newSet
|
|
val toAdd = newSet - currentSet
|
|
val toPromoteToUserSelection = currentSet
|
|
.intersect(requestedUserSamplingGeohashes)
|
|
.intersect(liveSamplingSubscriptionGeohashes)
|
|
|
|
if (toAdd.isEmpty() && toRemove.isEmpty() && toPromoteToUserSelection.isEmpty()) return
|
|
|
|
Log.d(TAG, "🌍 Updating sampling: +${toAdd.size} new, -${toRemove.size} removed")
|
|
|
|
// Remove old subscriptions
|
|
toRemove.forEach { geohash ->
|
|
unsubscribeSampling(geohash)
|
|
activeSamplingGeohashes.remove(geohash)
|
|
}
|
|
|
|
// A bookmark must remain functional after live access is revoked. Replace a
|
|
// live-tagged subscription with an untagged manual subscription immediately.
|
|
toPromoteToUserSelection.forEach { geohash ->
|
|
unsubscribeSampling(geohash)
|
|
if (isAppInForeground()) performSubscribeSampling(geohash)
|
|
}
|
|
|
|
// Add new subscriptions
|
|
activeSamplingGeohashes.addAll(toAdd)
|
|
if (isAppInForeground()) {
|
|
toAdd.forEach { geohash ->
|
|
performSubscribeSampling(geohash)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun endGeohashSampling() {
|
|
requestedLiveSamplingGeohashes = emptySet()
|
|
requestedUserSamplingGeohashes = emptySet()
|
|
if (activeSamplingGeohashes.isEmpty()) return
|
|
Log.d(TAG, "🌍 Ending geohash sampling (cleaning up ${activeSamplingGeohashes.size} subs)")
|
|
|
|
activeSamplingGeohashes.toList().forEach { geohash ->
|
|
unsubscribeSampling(geohash)
|
|
}
|
|
activeSamplingGeohashes.clear()
|
|
}
|
|
fun geohashParticipantCount(geohash: String): Int = repo.geohashParticipantCount(geohash)
|
|
fun isPersonTeleported(pubkeyHex: String): Boolean = repo.isPersonTeleported(pubkeyHex)
|
|
|
|
fun startGeohashDM(pubkeyHex: String, onStartPrivateChat: (String) -> Unit) {
|
|
val convKey = "nostr_${pubkeyHex.take(16)}"
|
|
repo.putNostrKeyMapping(convKey, pubkeyHex)
|
|
// Record the conversation's geohash using the currently selected location channel (if any)
|
|
val current = state.selectedLocationChannel.value
|
|
val gh = (current as? com.bitchat.android.geohash.ChannelID.Location)?.channel?.geohash
|
|
if (!gh.isNullOrEmpty()) {
|
|
repo.setConversationGeohash(convKey, gh)
|
|
GeohashConversationRegistry.set(convKey, gh)
|
|
}
|
|
onStartPrivateChat(convKey)
|
|
Log.d(TAG, "🗨️ Started geohash DM")
|
|
}
|
|
|
|
fun startGeohashDMByNickname(nickname: String, onStartPrivateChat: (String) -> Unit) {
|
|
val pubkey = repo.findPubkeyByNickname(nickname)
|
|
if (pubkey != null) {
|
|
startGeohashDM(pubkey, onStartPrivateChat)
|
|
} else {
|
|
Log.w(TAG, "Cannot start geohash DM: nickname '$nickname' not found in repo")
|
|
// Optionally notify user
|
|
}
|
|
}
|
|
|
|
fun startGeohashDMByShortId(shortId: String, onStartPrivateChat: (String) -> Unit) {
|
|
val pubkey = repo.findPubkeyByShortId(shortId)
|
|
if (pubkey != null) {
|
|
startGeohashDM(pubkey, onStartPrivateChat)
|
|
} else {
|
|
Log.w(TAG, "Cannot start geohash DM: shortId '$shortId' not found in repo")
|
|
}
|
|
}
|
|
|
|
fun getNostrKeyMapping(): Map<String, String> = repo.getNostrKeyMapping()
|
|
|
|
fun blockUserInGeohash(targetNickname: String) {
|
|
val pubkey = repo.findPubkeyByNickname(targetNickname)
|
|
if (pubkey != null) {
|
|
dataManager.addGeohashBlockedUser(pubkey)
|
|
// Refresh people list and counts to remove blocked entry immediately
|
|
repo.refreshGeohashPeople()
|
|
repo.updateReactiveParticipantCounts()
|
|
val sysMsg = com.bitchat.android.model.BitchatMessage(
|
|
sender = "system",
|
|
content = "blocked $targetNickname in geohash channels",
|
|
timestamp = Date(),
|
|
isRelay = false
|
|
)
|
|
messageManager.addMessage(sysMsg)
|
|
} else {
|
|
val sysMsg = com.bitchat.android.model.BitchatMessage(
|
|
sender = "system",
|
|
content = "user '$targetNickname' not found in current geohash",
|
|
timestamp = Date(),
|
|
isRelay = false
|
|
)
|
|
messageManager.addMessage(sysMsg)
|
|
}
|
|
}
|
|
|
|
fun selectLocationChannel(channel: com.bitchat.android.geohash.ChannelID) {
|
|
locationChannelManager?.select(channel) ?: run { Log.w(TAG, "Cannot select location channel - not initialized") }
|
|
}
|
|
|
|
fun ensureGeohashDMSubscriptionForConversation(conversationKey: String) {
|
|
val geohash = repo.getConversationGeohash(conversationKey)
|
|
?: NostrBackgroundRuntime.conversationGeohash(conversationKey)
|
|
?: return
|
|
NostrBackgroundRuntime.ensureConversationDm(geohash)
|
|
}
|
|
|
|
fun displayNameForNostrPubkeyUI(pubkeyHex: String): String {
|
|
val foregroundName = repo.displayNameForNostrPubkeyUI(pubkeyHex)
|
|
return foregroundName.takeUnless { it == "anon" }
|
|
?: NostrBackgroundRuntime.displayNameForNostrPubkey(pubkeyHex)
|
|
?: foregroundName
|
|
}
|
|
|
|
fun displayNameForGeohashConversation(pubkeyHex: String, sourceGeohash: String): String {
|
|
val foregroundName = repo.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)
|
|
return foregroundName.takeUnless { it == "anon" }
|
|
?: NostrBackgroundRuntime.displayNameForGeohashConversation(pubkeyHex, sourceGeohash)
|
|
?: foregroundName
|
|
}
|
|
|
|
fun conversationGeohash(conversationKey: String): String? =
|
|
repo.getConversationGeohash(conversationKey)
|
|
?: NostrBackgroundRuntime.conversationGeohash(conversationKey)
|
|
|
|
fun peerIdentityForNostrPubkey(pubkeyHex: String): PeerIdentity =
|
|
PeerIdentity.nostr(pubkeyHex)
|
|
|
|
private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
|
|
geoTimer?.cancel(); geoTimer = null
|
|
currentGeohashPresenceSubId?.let { subscriptionManager.unsubscribe(it); currentGeohashPresenceSubId = null }
|
|
|
|
when (channel) {
|
|
is com.bitchat.android.geohash.ChannelID.Mesh -> {
|
|
Log.d(TAG, "📡 Switched to mesh channel")
|
|
activeChannelGeohash = null
|
|
repo.setCurrentGeohash(null)
|
|
notificationManager.setCurrentGeohash(null)
|
|
notificationManager.clearMeshMentionNotifications()
|
|
repo.refreshGeohashPeople()
|
|
}
|
|
is com.bitchat.android.geohash.ChannelID.Location -> {
|
|
if (locationChannelManager?.canUseSelectedLocationChannel(channel.channel) != true) {
|
|
Log.w(TAG, "Ignoring a stale live-location channel selection")
|
|
switchLocationChannel(com.bitchat.android.geohash.ChannelID.Mesh)
|
|
return
|
|
}
|
|
Log.d(TAG, "📍 Switching to geohash channel")
|
|
activeChannelGeohash = channel.channel.geohash
|
|
repo.setCurrentGeohash(channel.channel.geohash)
|
|
repo.refreshGeohashPeople()
|
|
notificationManager.setCurrentGeohash(channel.channel.geohash)
|
|
notificationManager.clearNotificationsForGeohash(channel.channel.geohash)
|
|
try { messageManager.clearChannelUnreadCount("geo:${channel.channel.geohash}") } catch (_: Exception) { }
|
|
|
|
try {
|
|
val identity = NostrIdentityBridge.deriveIdentity(channel.channel.geohash, getApplication())
|
|
// We don't update participant here anymore; presence loop handles it via Kind 20001
|
|
val teleported = locationChannelManager?.teleported?.value
|
|
?: state.isTeleported.value
|
|
if (teleported) repo.markTeleported(identity.publicKeyHex)
|
|
} catch (e: Exception) { Log.w(TAG, "Failed identity setup: ${e.message}") }
|
|
|
|
startGeoParticipantsTimer()
|
|
val liveLocationToken = locationChannelManager
|
|
?.liveLocationTokenForSelectedChannel(channel.channel)
|
|
|
|
// Presence heartbeat firehose (kind 20001) is the high-volume data hog; only
|
|
// run it in the foreground. It is restored in onStart() and torn down in onStop().
|
|
if (isAppInForeground()) {
|
|
subscribeChannelPresence(channel.channel.geohash, liveLocationToken)
|
|
}
|
|
}
|
|
null -> {
|
|
Log.d(TAG, "📡 No channel selected")
|
|
repo.setCurrentGeohash(null)
|
|
repo.refreshGeohashPeople()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribe to the presence heartbeat firehose (kind 20001) for a geohash channel.
|
|
* High-volume; only used to refresh the participant list, so it is torn down in
|
|
* onStop() and restored in onStart() to cut background mobile data.
|
|
*/
|
|
private fun subscribeChannelPresence(
|
|
geohash: String,
|
|
liveLocationToken: Long?
|
|
) {
|
|
val subId = "geohash-presence-${UUID.randomUUID()}"; currentGeohashPresenceSubId = subId
|
|
subscriptionManager.subscribeGeohashPresence(
|
|
geohash = geohash,
|
|
sinceMs = System.currentTimeMillis() - 3600000L,
|
|
limit = 200,
|
|
id = subId,
|
|
handler = { event -> geohashMessageHandler.onEvent(event, geohash) },
|
|
liveLocationToken = liveLocationToken
|
|
)
|
|
}
|
|
|
|
private fun startGeoParticipantsTimer() {
|
|
geoTimer = viewModelScope.launch {
|
|
while (repo.getCurrentGeohash() != null) {
|
|
delay(30000)
|
|
repo.refreshGeohashPeople()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCleared() {
|
|
shutdownUiSubscriptions()
|
|
super.onCleared()
|
|
}
|
|
|
|
fun shutdownUiSubscriptions() {
|
|
if (uiSubscriptionsShutdown) return
|
|
uiSubscriptionsShutdown = true
|
|
subscriptionManager.unsubscribeAllOwned()
|
|
geoTimer?.cancel()
|
|
geoTimer = null
|
|
kotlin.runCatching {
|
|
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
|
|
}
|
|
LiveLocationPrivacyGate.removeRevocationListener(liveLocationRevocationListener)
|
|
}
|
|
|
|
override fun onStart(owner: LifecycleOwner) {
|
|
Log.d(TAG, "🌍 App foregrounded: resuming Nostr streaming")
|
|
// Android permission may have changed while backgrounded. Invalidate the
|
|
// process-wide token before restoring any subscription or heartbeat.
|
|
locationChannelManager?.syncPermissionState()
|
|
|
|
// Restore the presence heartbeat firehose for the selected geohash channel.
|
|
// (The chat message stream is kept alive in the background, so it is not restored here.)
|
|
val selected = locationChannelManager?.selectedChannel?.value
|
|
val selectedLocation = selected as? com.bitchat.android.geohash.ChannelID.Location
|
|
if (selectedLocation != null &&
|
|
selectedLocation.channel.geohash == activeChannelGeohash &&
|
|
locationChannelManager?.canUseSelectedLocationChannel(selectedLocation.channel) == true
|
|
) {
|
|
subscribeChannelPresence(
|
|
selectedLocation.channel.geohash,
|
|
locationChannelManager
|
|
?.liveLocationTokenForSelectedChannel(selectedLocation.channel)
|
|
)
|
|
}
|
|
// Resume geohash sampling subscriptions
|
|
activeSamplingGeohashes.forEach { performSubscribeSampling(it) }
|
|
// Resume the participant-refresh polling timer if a geohash is selected
|
|
if (repo.getCurrentGeohash() != null && geoTimer?.isActive != true) {
|
|
startGeoParticipantsTimer()
|
|
}
|
|
}
|
|
|
|
override fun onStop(owner: LifecycleOwner) {
|
|
Log.d(TAG, "🌍 App backgrounded: pausing geohash presence firehose (keeping message + DM subscriptions)")
|
|
// Drop the high-volume presence heartbeat firehose (kind 20001).
|
|
// The chat message stream (kind 20000) is intentionally left active so messages still arrive.
|
|
currentGeohashPresenceSubId?.let { subscriptionManager.unsubscribe(it); currentGeohashPresenceSubId = null }
|
|
// Drop geohash sampling subscriptions
|
|
activeSamplingGeohashes.forEach(::unsubscribeSampling)
|
|
// Stop participant-refresh polling
|
|
geoTimer?.cancel(); geoTimer = null
|
|
// Process-owned message and DM subscriptions remain active.
|
|
}
|
|
|
|
private fun performSubscribeSampling(geohash: String) {
|
|
val subscriptionId = samplingSubscriptionIds.getOrPut(geohash) {
|
|
"sampling-${UUID.randomUUID()}"
|
|
}
|
|
// Sampling only needs participant counts, never message bodies, so it subscribes to
|
|
// presence heartbeats only (kind 20001) to keep the payload small.
|
|
val subscribe = {
|
|
liveSamplingSubscriptionGeohashes.remove(geohash)
|
|
subscriptionManager.subscribeGeohashPresence(
|
|
geohash = geohash,
|
|
sinceMs = System.currentTimeMillis() - 86400000L,
|
|
limit = 200,
|
|
id = subscriptionId,
|
|
handler = { event -> geohashMessageHandler.onEvent(event, geohash) }
|
|
)
|
|
}
|
|
|
|
if (geohash in requestedUserSamplingGeohashes) {
|
|
subscribe()
|
|
} else if (geohash in requestedLiveSamplingGeohashes) {
|
|
val isCurrentLiveTarget = locationChannelManager
|
|
?.availableChannels
|
|
?.value
|
|
?.any { it.geohash == geohash } == true
|
|
if (!isCurrentLiveTarget) return
|
|
val token = LiveLocationPrivacyGate.captureToken() ?: return
|
|
LiveLocationPrivacyGate.runIfAllowed(token) {
|
|
liveSamplingSubscriptionGeohashes.add(geohash)
|
|
subscriptionManager.subscribeGeohashPresence(
|
|
geohash = geohash,
|
|
sinceMs = System.currentTimeMillis() - 86400000L,
|
|
limit = 200,
|
|
id = subscriptionId,
|
|
handler = { event -> geohashMessageHandler.onEvent(event, geohash) },
|
|
liveLocationToken = token
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun unsubscribeSampling(geohash: String) {
|
|
samplingSubscriptionIds.remove(geohash)?.let(subscriptionManager::unsubscribe)
|
|
liveSamplingSubscriptionGeohashes.remove(geohash)
|
|
}
|
|
|
|
private fun isAppInForeground(): Boolean {
|
|
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
|
}
|
|
}
|