Onion paths as a StateFlow (#901)

pull/1709/head
SessionHero01 3 months ago committed by GitHub
parent 0dc8aa1410
commit e5e00c4548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -183,7 +183,6 @@ import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.openUrl
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
@ -2071,28 +2070,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
private fun informUserIfNetworkOrSessionNodePathIsInvalid() {
// Check that we have a valid network network connection & inform the user if not
val connectedToInternet = NetworkUtils.haveValidNetworkConnection(applicationContext)
if (!connectedToInternet)
{
// TODO: Adjust to display error to user with official localised string when SES-2319 is addressed
Log.e(TAG, "Cannot sent voice message - no network connection.")
}
// Check that we have a suite of Session Nodes to route through.
// Note: We can have the entry node plus the 2 Session Nodes and the data _still_ might not
// send due to any node flakiness - but without doing some manner of test-ping through
// there's no way to test our client -> destination connectivity (unless we abuse the typing
// indicators?)
val paths = OnionRequestAPI.paths
if (paths.isNullOrEmpty() || paths.count() != 2) {
// TODO: Adjust to display error to user with official localised string when SES-2319 is addressed
Log.e(TAG, "Cannot send voice message - bad Session Node path.")
}
}
override fun sendVoiceMessage() {
// When the record voice message button is released we always need to reset the UI and cancel
// any further recording operation..
@ -2119,7 +2096,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
return
}
informUserIfNetworkOrSessionNodePathIsInvalid()
// Note: We could return here if there was a network or node path issue, but instead we'll try
// our best to send the voice message even if it might fail - because in that case it'll get put
// into the draft database and can be retried when we regain network connectivity and a working

@ -16,11 +16,17 @@ import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -64,25 +70,21 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
registerObservers()
IP2Country.configureIfNeeded(this)
}
private fun registerObservers() {
val buildingPathsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handleBuildingPathsEvent()
lifecycleScope.launch {
// Check if the
repeatOnLifecycle(Lifecycle.State.STARTED) {
OnionRequestAPI.paths
.map { it.isEmpty() }
.distinctUntilChanged()
.collectLatest {
update(true)
}
}
broadcastReceivers.add(buildingPathsReceiver)
LocalBroadcastManager.getInstance(this).registerReceiver(buildingPathsReceiver, IntentFilter("buildingPaths"))
val pathsBuiltReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handlePathsBuiltEvent()
}
}
broadcastReceivers.add(pathsBuiltReceiver)
LocalBroadcastManager.getInstance(this).registerReceiver(pathsBuiltReceiver, IntentFilter("pathsBuilt"))
private fun registerObservers() {
val onionRequestPathCountriesLoadedReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@ -102,15 +104,15 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
// endregion
// region Updating
private fun handleBuildingPathsEvent() { update(false) }
private fun handlePathsBuiltEvent() { update(false) }
private fun handleOnionRequestPathCountriesLoaded() { update(false) }
private fun update(isAnimated: Boolean) {
binding.pathRowsContainer.removeAllViews()
if (OnionRequestAPI.paths.isNotEmpty()) {
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
val paths = OnionRequestAPI.paths.value
if (paths.isNotEmpty()) {
val path = paths.firstOrNull() ?: return finish()
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
val pathRows = path.mapIndexed { index, snode ->
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))

@ -1,27 +1,23 @@
package org.thoughtcrime.securesms.home
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.lifecycle.coroutineScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
import org.thoughtcrime.securesms.util.toPx
class PathStatusView : View {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
@ColorInt var mainColor: Int = 0
set(newValue) { field = newValue; paint.color = newValue }
@ColorInt var sessionShadowColor: Int = 0
@ -53,51 +49,16 @@ class PathStatusView : View {
}
private fun initialize() {
if (!isInEditMode) {
update()
}
setWillNotDraw(false)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
registerObservers()
}
private fun registerObservers() {
val buildingPathsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handleBuildingPathsEvent()
}
}
broadcastReceivers.add(buildingPathsReceiver)
LocalBroadcastManager.getInstance(context).registerReceiver(buildingPathsReceiver, IntentFilter("buildingPaths"))
val pathsBuiltReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handlePathsBuiltEvent()
}
}
broadcastReceivers.add(pathsBuiltReceiver)
LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltReceiver, IntentFilter("pathsBuilt"))
}
override fun onDetachedFromWindow() {
for (receiver in broadcastReceivers) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
}
super.onDetachedFromWindow()
}
private fun handleBuildingPathsEvent() { update() }
private fun handlePathsBuiltEvent() { update() }
private fun update() {
if (updateJob?.isActive != true) { // false or null
updateJob = ViewUtil.getActivityLifecycle(this)?.coroutineScope?.launchWhenStarted {
val paths = withContext(Dispatchers.IO) { OnionRequestAPI.paths }
if (paths.isNotEmpty()) {
updateJob = GlobalScope.launch(Dispatchers.Main) {
OnionRequestAPI.hasPath
.collectLatest { pathsBuilt ->
if (pathsBuilt) {
setBackgroundResource(R.drawable.accent_dot)
val hasPathsColor = context.getColor(R.color.accent_green)
mainColor = hasPathsColor
@ -112,6 +73,12 @@ class PathStatusView : View {
}
}
override fun onDetachedFromWindow() {
updateJob?.cancel()
super.onDetachedFromWindow()
}
override fun onDraw(c: Canvas) {
val w = width.toFloat()
val h = height.toFloat()

@ -103,7 +103,6 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.NetworkUtils
import org.thoughtcrime.securesms.util.push
import java.io.File
@ -429,7 +428,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))
val hasPaths by hasPaths().collectAsState(initial = false)
val hasPaths by OnionRequestAPI.hasPath.collectAsState()
Cell {
Column {
@ -620,19 +619,3 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
}
}
private fun Context.hasPaths(): Flow<Boolean> = LocalBroadcastManager.getInstance(this).hasPaths()
private fun LocalBroadcastManager.hasPaths(): Flow<Boolean> = callbackFlow {
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { trySend(Unit) }
}
registerReceiver(receiver, IntentFilter("buildingPaths"))
registerReceiver(receiver, IntentFilter("pathsBuilt"))
awaitClose { unregisterReceiver(receiver) }
}.onStart { emit(Unit) }.map {
withContext(Dispatchers.Default) {
OnionRequestAPI.paths.isNotEmpty()
}
}

@ -7,6 +7,8 @@ import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.opencsv.CSVReader
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.Log
@ -72,17 +74,14 @@ class IP2Country internal constructor(
if (isInitialized) { return; }
shared = IP2Country(context.applicationContext)
val pathsBuiltEventReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
GlobalScope.launch {
OnionRequestAPI.paths
.filter { it.isNotEmpty() }
.collectLatest {
shared.populateCacheIfNeeded()
}
}
LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltEventReceiver, IntentFilter("pathsBuilt"))
}
}
init {
populateCacheIfNeeded()
}
// TODO: Deinit?
@ -105,16 +104,14 @@ class IP2Country internal constructor(
}
private fun populateCacheIfNeeded() {
GlobalScope.launch {
val start = System.currentTimeMillis()
OnionRequestAPI.paths.iterator().forEach { path ->
OnionRequestAPI.paths.value.iterator().forEach { path ->
path.iterator().forEach { snode ->
cacheCountryForIP(snode.ip) // Preload if needed
}
}
Log.d("Loki","Cache populated in ${System.currentTimeMillis() - start}ms")
Log.d("Loki","IP2Country cache populated in ${System.currentTimeMillis() - start}ms")
Broadcaster(context).broadcast("onionRequestPathCountriesLoaded")
}
}
// endregion
}

@ -2,6 +2,13 @@ package org.session.libsession.snode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise
@ -40,35 +47,31 @@ object OnionRequestAPI {
private var buildPathsPromise: Promise<List<Path>, Exception>? = null
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
private val broadcaster: Broadcaster
get() = SnodeModule.shared.broadcaster
private val pathFailureCount = mutableMapOf<Path, Int>()
private val snodeFailureCount = mutableMapOf<Snode, Int>()
var guardSnodes = setOf<Snode>()
var _paths: AtomicReference<List<Path>?> = AtomicReference(null)
var paths: List<Path> // Not a Set to ensure we consistently show the same path to the user
@Synchronized
get() {
val paths = _paths.get()
if (paths != null) { return paths }
// Storing this in an atomic variable as it was causing a number of background
// ANRs when this value was accessed via the main thread after tapping on
// a notification)
val result = database.getOnionRequestPaths()
_paths.set(result)
return result
}
@Synchronized
set(newValue) {
if (newValue.isEmpty()) {
private val mutablePaths = MutableStateFlow(database.getOnionRequestPaths())
val paths: StateFlow<List<Path>> get() = mutablePaths
val hasPath: StateFlow<Boolean> = mutablePaths
.drop(1)
.map { it.isNotEmpty() }
.stateIn(GlobalScope, SharingStarted.Eagerly, paths.value.isNotEmpty())
init {
// Listen for the changes in paths and persist it to the db
GlobalScope.launch {
mutablePaths
.drop(1) // Drop the first result where it just comes from the db
.collectLatest {
if (it.isEmpty()) {
database.clearOnionRequestPaths()
_paths.set(null)
} else {
database.setOnionRequestPaths(newValue)
_paths.set(newValue)
database.setOnionRequestPaths(it)
}
}
}
}
@ -172,7 +175,6 @@ object OnionRequestAPI {
val existingBuildPathsPromise = buildPathsPromise
if (existingBuildPathsPromise != null) { return existingBuildPathsPromise }
Log.d("Loki", "Building onion request paths.")
broadcaster.broadcast("buildingPaths")
val promise = SnodeAPI.getRandomSnode().bind { // Just used to populate the snode pool
val reusableGuardSnodes = reusablePaths.map { it[0] }
getGuardSnodes(reusableGuardSnodes).map { guardSnodes ->
@ -193,8 +195,7 @@ object OnionRequestAPI {
result
}
}.map { paths ->
OnionRequestAPI.paths = paths + reusablePaths
broadcaster.broadcast("pathsBuilt")
mutablePaths.value = paths + reusablePaths
paths
}
}
@ -209,7 +210,7 @@ object OnionRequestAPI {
*/
private fun getPath(snodeToExclude: Snode?): Promise<Path, Exception> {
if (pathSize < 1) { throw Exception("Can't build path of size zero.") }
val paths = this.paths
val paths = this.paths.value
val guardSnodes = mutableSetOf<Snode>()
if (paths.isNotEmpty()) {
guardSnodes.add(paths[0][0])
@ -256,7 +257,7 @@ object OnionRequestAPI {
// path we leave the re-building up to getPath() because re-building the path in that case
// is async.
snodeFailureCount[snode] = 0
val oldPaths = paths.toMutableList()
val oldPaths = mutablePaths.value.toMutableList()
val pathIndex = oldPaths.indexOfFirst { it.contains(snode) }
if (pathIndex == -1) { return }
val path = oldPaths[pathIndex].toMutableList()
@ -269,16 +270,16 @@ object OnionRequestAPI {
// Don't test the new snode as this would reveal the user's IP
oldPaths.removeAt(pathIndex)
val newPaths = oldPaths + listOf( path )
paths = newPaths
mutablePaths.value = newPaths
}
private fun dropPath(path: Path) {
pathFailureCount[path] = 0
val paths = OnionRequestAPI.paths.toMutableList()
val paths = mutablePaths.value.toMutableList()
val pathIndex = paths.indexOf(path)
if (pathIndex == -1) { return }
paths.removeAt(pathIndex)
OnionRequestAPI.paths = paths
mutablePaths.value = paths
}
/**
@ -369,7 +370,7 @@ object OnionRequestAPI {
val checkedGuardSnode = guardSnode
val path =
if (checkedGuardSnode == null) null
else paths.firstOrNull { it.contains(checkedGuardSnode) }
else paths.value.firstOrNull { it.contains(checkedGuardSnode) }
fun handleUnspecificError() {
if (path == null) { return }

Loading…
Cancel
Save