diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 132c605637..23146cb256 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt index 83c7e0cc86..fd9e362e9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt @@ -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)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt index 1c31a2ee17..a422ddf9e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt @@ -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() @ColorInt var mainColor: Int = 0 set(newValue) { field = newValue; paint.color = newValue } @ColorInt var sessionShadowColor: Int = 0 @@ -53,65 +49,36 @@ 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() - } + 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 + sessionShadowColor = hasPathsColor + } else { + setBackgroundResource(R.drawable.paths_building_dot) + val pathsBuildingColor = ContextCompat.getColor(context, R.color.paths_building) + mainColor = pathsBuildingColor + sessionShadowColor = pathsBuildingColor + } + } } - broadcastReceivers.add(pathsBuiltReceiver) - LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltReceiver, IntentFilter("pathsBuilt")) } + override fun onDetachedFromWindow() { - for (receiver in broadcastReceivers) { - LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver) - } + updateJob?.cancel() 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()) { - setBackgroundResource(R.drawable.accent_dot) - val hasPathsColor = context.getColor(R.color.accent_green) - mainColor = hasPathsColor - sessionShadowColor = hasPathsColor - } else { - setBackgroundResource(R.drawable.paths_building_dot) - val pathsBuildingColor = ContextCompat.getColor(context, R.color.paths_building) - mainColor = pathsBuildingColor - sessionShadowColor = pathsBuildingColor - } - } - } - } - override fun onDraw(c: Canvas) { val w = width.toFloat() val h = height.toFloat() diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index fdb58078ee..7ed588f8ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -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 = LocalBroadcastManager.getInstance(this).hasPaths() -private fun LocalBroadcastManager.hasPaths(): Flow = 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() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt index ce5b8916cc..645566b42b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt @@ -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,19 +74,16 @@ class IP2Country internal constructor( if (isInitialized) { return; } shared = IP2Country(context.applicationContext) - val pathsBuiltEventReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - shared.populateCacheIfNeeded() - } + GlobalScope.launch { + OnionRequestAPI.paths + .filter { it.isNotEmpty() } + .collectLatest { + shared.populateCacheIfNeeded() + } } - LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltEventReceiver, IntentFilter("pathsBuilt")) } } - init { - populateCacheIfNeeded() - } - // TODO: Deinit? // endregion @@ -105,16 +104,14 @@ class IP2Country internal constructor( } private fun populateCacheIfNeeded() { - GlobalScope.launch { - val start = System.currentTimeMillis() - OnionRequestAPI.paths.iterator().forEach { path -> - path.iterator().forEach { snode -> - cacheCountryForIP(snode.ip) // Preload if needed - } + val start = System.currentTimeMillis() + 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") - Broadcaster(context).broadcast("onionRequestPathCountriesLoaded") } + Log.d("Loki","IP2Country cache populated in ${System.currentTimeMillis() - start}ms") + Broadcaster(context).broadcast("onionRequestPathCountriesLoaded") } // endregion } diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 45b004414c..7357c1f1aa 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -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,37 +47,33 @@ object OnionRequestAPI { private var buildPathsPromise: Promise, Exception>? = null private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage - private val broadcaster: Broadcaster - get() = SnodeModule.shared.broadcaster private val pathFailureCount = mutableMapOf() private val snodeFailureCount = mutableMapOf() var guardSnodes = setOf() - var _paths: AtomicReference?> = AtomicReference(null) - var paths: List // 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()) { - database.clearOnionRequestPaths() - _paths.set(null) - } else { - database.setOnionRequestPaths(newValue) - _paths.set(newValue) + + private val mutablePaths = MutableStateFlow(database.getOnionRequestPaths()) + + val paths: StateFlow> get() = mutablePaths + val hasPath: StateFlow = 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() + } else { + database.setOnionRequestPaths(it) + } } } + } // region Settings /** @@ -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 { if (pathSize < 1) { throw Exception("Can't build path of size zero.") } - val paths = this.paths + val paths = this.paths.value val guardSnodes = mutableSetOf() 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 }