From 314ea9aef48c1d389d3d00985145afcd444e5c68 Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Thu, 30 Apr 2026 19:03:41 +0100 Subject: [PATCH 1/2] Fixed and normalized polling from a single source. Added a remote config to modulate the sync and non sync durations update the core module --- .../data/repository/LtcRepository.kt | 3 +- .../data/repository/LtcRepositoryImpl.kt | 72 +++++++++++++++++-- .../data/source/RemoteConfigSource.kt | 8 +++ .../presenter/activities/util/BRActivity.java | 10 --- .../ltcpickerbento/LTCPickerBentoViewModel.kt | 69 ++++++++---------- .../worker/CurrencyUpdateWorker.kt | 33 --------- app/src/main/jni/core | 2 +- .../main/res/xml/remote_config_defaults.xml | 54 +++++++++++--- 8 files changed, 153 insertions(+), 98 deletions(-) delete mode 100644 app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 5d17e732..ddab8a08 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -5,11 +5,12 @@ import com.brainwallet.data.model.Fee import com.brainwallet.data.model.LtcStats import com.brainwallet.data.model.MoonpayCurrencyLimit import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow interface LtcRepository { - val rates: StateFlow> + val rates: SharedFlow> val ltcStats: StateFlow suspend fun fetchRates(): List diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt index 0f315d0f..7628af17 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt @@ -10,28 +10,56 @@ import com.brainwallet.data.model.LtcStats import com.brainwallet.data.model.MoonpayCurrencyLimit import com.brainwallet.data.repository.LtcRepository.Companion.PREF_KEY_BUY_LIMITS_PREFIX import com.brainwallet.data.repository.LtcRepository.Companion.PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT +import com.brainwallet.data.source.PeerManagerSource import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.data.source.fetchWithCache import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import com.brainwallet.di.AppModule.json import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.tools.util.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import org.koin.core.annotation.Single +import timber.log.Timber @Single(binds = [LtcRepository::class]) class LtcRepositoryImpl( private val context: Context, private val remoteApiSource: RemoteApiSource, + private val remoteConfigSource: RemoteConfigSource, private val currencyDataSource: CurrencyDataSource, + private val peerManagerSource: PeerManagerSource, + private val repositoryScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO), private val sharedPreferences: SharedPreferences, ) : LtcRepository { - private val _rates = MutableStateFlow>(emptyList()) - override val rates: StateFlow> = _rates.asStateFlow() +// private val _rates = MutableStateFlow>( +// currencyDataSource.getAllCurrencies(true) // seed from cache immediately +// ) + private val _rates = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ).apply { + // seed with cached values immediately, same as before + tryEmit(currencyDataSource.getAllCurrencies(true)) + } + override val rates: SharedFlow> = _rates.asSharedFlow() private val _ltcStats = MutableStateFlow( LtcStats( 0, @@ -40,12 +68,48 @@ class LtcRepositoryImpl( 0 ) ) + + private var syncingDelay = 60_000L + private var nonSyncingDelay = 4_000L + + @Serializable + private data class SyncPoolerContentWrapper( + @SerialName("sync_delay") val syncDelayValue: Long = 60_000L, + @SerialName("non_sync_delay") val nonSyncDelayValue: Long = 4_000L + ) + override val ltcStats: StateFlow = _ltcStats.asStateFlow() + init { + + repositoryScope.launch { + runCatching { remoteConfigSource.fetchAndActivate() } + .onFailure { Timber.e(it, "RemoteConfig fetch failed") } + + val rawJson = remoteConfigSource.getString(RemoteConfigSource.KEY_SYNC_POLLER) + runCatching { + val config = json.decodeFromString(rawJson) + syncingDelay = config.syncDelayValue + nonSyncingDelay = config.nonSyncDelayValue + }.onFailure { Timber.e(it, "RemoteConfig parse failed") } + + while (isActive) { + runCatching { fetchRates() } + .onFailure { Timber.e(it, "fetchRates failed") } + + val delay = if (peerManagerSource.blockInfo.value.syncProgress <= 0.95f) { + syncingDelay + } else { + nonSyncingDelay + } + Timber.d("timber|| duration in fetching $syncingDelay $nonSyncingDelay") + delay(delay) + } + } + } override suspend fun fetchRates(): List { return runCatching { val rates = remoteApiSource.getRates() - FeeManager.updateFeePerKb(context) val selectedISO = BRSharedPrefs.getIsoSymbol(context) rates.forEachIndexed { index, currencyEntity -> @@ -62,7 +126,7 @@ class LtcRepositoryImpl( }.getOrElse { currencyDataSource.getAllCurrencies(true) }.also { result -> - _rates.value = result + _rates.emit(result) } } diff --git a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt index 11085d27..7de5a4d8 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt @@ -8,6 +8,14 @@ interface RemoteConfigSource { const val KEY_FEATURE_MENU_HIDDEN_EXAMPLE = "feature_menu_hidden_example" const val KEY_FEATURE_SELECTED_PEERS_ENABLED = "feature_selected_peers_enabled" const val KEY_FEATURE_GAMEHUB_CONTENT = "feature_gamehub_content" + const val PATH_SHOP_CONTENT = "path_shop_content" + const val KEY_SYNC_POLLER = "key_sync_poller" + const val KEY_API_BASEURL_PROD_NEW_ENABLED = "key_api_baseurl_prod_new_enabled" + const val KEY_FEATURE_LTC_BROWSER_CONTENT = "feature_ltc_browser_content" + const val KEY_KEYSTORE_MANAGER_ENABLED = "key_keystore_manager_enabled" + const val KEY_DEV_API_BASEURL = "key_dev_api_baseurl" + const val KEY_API_BASEURL_DEV_NEW_ENABLED = "key_api_baseurl_dev_new_enabled" + const val KEY_PROD_API_BASEURL = "key_prod_api_baseurl" } fun initialize() diff --git a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java index 6644a386..8f3e93f1 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java @@ -9,12 +9,9 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.os.LocaleListCompat; - import com.brainwallet.BrainwalletApp; import com.brainwallet.data.repository.SettingRepository; import com.brainwallet.presenter.activities.DisabledActivity; -import com.brainwallet.presenter.activities.intro.RecoverActivity; -import com.brainwallet.presenter.activities.intro.WriteDownActivity; import com.brainwallet.tools.animation.BRAnimator; import com.brainwallet.tools.manager.InternetManager; import com.brainwallet.tools.security.AuthManager; @@ -24,7 +21,6 @@ import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.constants.BWConstants; import com.brainwallet.wallet.BRWalletManager; -import com.brainwallet.worker.CurrencyUpdateWorker; import org.koin.java.KoinJavaComponent; @@ -144,12 +140,6 @@ public void run() { public static void init(Activity app) { InternetManager.getInstance(); - - if (!(app instanceof RecoverActivity || app instanceof WriteDownActivity)) { - CurrencyUpdateWorker currencyUpdateWorker = KoinJavaComponent.get(CurrencyUpdateWorker.class); - currencyUpdateWorker.start(); - } - //show wallet locked if it is if (!ActivityUTILS.isAppSafe(app)) if (AuthManager.getInstance().isWalletDisabled(app)) diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt index 1517c3e2..a2128ac6 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt @@ -7,8 +7,6 @@ import com.brainwallet.data.repository.SettingRepository import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.ui.BrainwalletViewModel import com.brainwallet.util.CurrencyDataGetter -import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -34,15 +32,6 @@ class LTCPickerBentoViewModel( ) init { - viewModelScope.launch { - while (true) { - try { fetchAndUpdateRates() } catch (e: Exception) { - Timber.e(e, "fetchRates failed") - FirebaseCrashlytics.getInstance().recordException(e) - } - delay(5000L) - } - } viewModelScope.launch { settingRepository.settings.collect { setting -> _state.update { @@ -64,37 +53,38 @@ class LTCPickerBentoViewModel( } } } - } - private suspend fun fetchAndUpdateRates( - currencyCode: String = settingRepository.currentSettings.value.currency.code - ) { - val rates = ltcRepository.fetchRates() - val selectedFiat = rates.find { it.code == currencyCode } - val iso = currencyDataGetter.getIsoSymbol() - var formattedCurrency: String? = null - val currency = currencyDataGetter.getCurrencyByIso(iso) - if (currency != null) { - val roundedPriceAmount: BigDecimal = - BigDecimal(currency.rate.toDouble()).multiply(BigDecimal(100)) - .divide(BigDecimal(100), 2, BWConstants.ROUNDING_MODE) - formattedCurrency = - currencyDataGetter.getFormattedCurrencyString( - iso, - roundedPriceAmount - ) - } else { - Timber.w("The currency related to %s is NULL", iso) - } - _state.update { - it.copy( - selectedCurrency = selectedFiat ?: return@update it, - formattedFiat = formattedCurrency ?: "", - formattedTimeStamp = formatter.format(java.util.Date()), - ltcStats = ltcRepository.ltcStats.value - ) + viewModelScope.launch { + ltcRepository.rates.collect { rates -> + runCatching { + Timber.d("timber|| rates in fetching $rates") + val currencyCode = _state.value.selectedCurrency.code + val selectedFiat = rates.find { it.code == currencyCode } + val iso = currencyDataGetter.getIsoSymbol() + val currency = currencyDataGetter.getCurrencyByIso(iso) + + val formattedCurrency = currency?.let { + val rounded = BigDecimal(it.rate.toDouble()) + .multiply(BigDecimal(100)) + .divide(BigDecimal(100), 2, BWConstants.ROUNDING_MODE) + currencyDataGetter.getFormattedCurrencyString(iso, rounded) + } + + if (selectedFiat != null) { + _state.update { + it.copy( + selectedCurrency = selectedFiat, + formattedFiat = formattedCurrency ?: "", + formattedTimeStamp = formatter.format(java.util.Date()), + ltcStats = ltcRepository.ltcStats.value + ) + } + } + }.onFailure { Timber.e(it, "timber|| rates collector crashed") } + } } } + override fun onEvent(event: LTCPickerBentoEvent) { when (event) { is LTCPickerBentoEvent.OnGlobalCurrencyChange -> { @@ -102,7 +92,6 @@ class LTCPickerBentoViewModel( if (newCurrency != null) { viewModelScope.launch { settingRepository.save(settingRepository.currentSettings.value.copy(currency = newCurrency)) - fetchAndUpdateRates(newCurrency.code) } } else { Timber.w("Currency not found for code: ${event.globalCurrency.code}") diff --git a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt b/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt deleted file mode 100644 index c2f1f6ce..00000000 --- a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.brainwallet.worker - -import com.brainwallet.data.repository.LtcRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.koin.core.annotation.Single - -@Single -class CurrencyUpdateWorker( - private val ltcRepository: LtcRepository, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - private val workerScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) -) { - private var job: Job? = null - - fun start() { - if (job?.isActive == true) { - job?.cancel() - } - job = workerScope.launch(ioDispatcher) { - while (isActive) { - ltcRepository.fetchRates() - delay(4000L) - } - } - } -} diff --git a/app/src/main/jni/core b/app/src/main/jni/core index 28d7d31a..31a7122e 160000 --- a/app/src/main/jni/core +++ b/app/src/main/jni/core @@ -1 +1 @@ -Subproject commit 28d7d31ae057bdde33b1d19e6b7af3ba07710c89 +Subproject commit 31a7122e7ac47da290a5425bc637a4f21b9a2ca9 diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml index 6852653a..632af3b8 100644 --- a/app/src/main/res/xml/remote_config_defaults.xml +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -1,19 +1,55 @@ - + feature_selected_peers_enabled true - - + + update_debounce_interval 3.0 - - + + progress_update_interval 1.0 - - + + feature_menu_hidden_example {"enabled":true,"title":"brainwallet-android repository","url":"https://github.com/brainwallet-co/android"} - - \ No newline at end of file + + + key_sync_poller + {"delays":{"sync_delay":60000,"non_sync_delay":4000} + + + path_shop_content + https://www.bitrefill.com/?ref=bAshL935 + + + key_api_baseurl_prod_new_enabled + true + + + feature_ltc_browser_content + { "browsers": [ { "name" : "blockchair", "urlstub" : "https://blockchair.com/litecoin/transaction/" }, { "name" : "blockcypher", "urlstub" : "https://blockchair.com/litecoin/transaction/" }, { "name" : "litecoinspace", "urlstub" : "https://litecoinspace.org/address/" }, { "name" : "sochain", "urlstub" : "https://chain.so/address/LTC/" } ] } + + + feature_gamehub_content + { "hubcontent": [ { "content_name": "rice_partner_05", "asset_url": "https://storage.googleapis.com/1756210308-bw-public/rice_partner_05.jpg", "title": "MoonPay", "subtitle": "Top up in 5 minutes!", "id": 10000 }, { "content_name": "unagi_social_02", "asset_url": "https://storage.googleapis.com/1756210308-bw-public/unagi_social_02.jpg", "title": "Social media background", "subtitle": "Follow us on socials!", "id": 20000 } ] } + + + key_keystore_manager_enabled + true + + + key_dev_api_baseurl + https://dev.apigsltd.net/ + + + key_api_baseurl_dev_new_enabled + true + + + key_prod_api_baseurl + https://prod.apigsltd.net/ + + From 3db890b3bdf03c3d6110be92a25124626318f56d Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Fri, 1 May 2026 23:42:42 +0100 Subject: [PATCH 2/2] Refactored the webviews --- .../com/brainwallet/constants/BWConstants.kt | 2 +- .../brainwallet/ui/screens/main/MainScreen.kt | 2 ++ .../ui/screens/main/WebModalScreen.kt | 33 ++++++++++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/brainwallet/constants/BWConstants.kt b/app/src/main/java/com/brainwallet/constants/BWConstants.kt index b4dc267b..48836c3d 100644 --- a/app/src/main/java/com/brainwallet/constants/BWConstants.kt +++ b/app/src/main/java/com/brainwallet/constants/BWConstants.kt @@ -105,7 +105,7 @@ object BWConstants { const val TOS_LINK: String = "https://www.brainwallet.co/privacypolicy" const val LINKTREE_URL: String = "https://linktr.ee/brainwallet" - const val BITREFILL_URL: String = "https://www.bitrefill.com/?ref=bAshL935" + const val BITREFILL_URL: String = "https://www.bitrefill.com/" /** * API Hosts diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt index 3c37efeb..f5340c08 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt @@ -454,6 +454,8 @@ fun MainScreen( ) Route.BitrefillWeb -> WebModalScreen( + modifier = Modifier + .fillMaxSize(), onNavigate = onNavigate, url = BWConstants.BITREFILL_URL ) diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt index 58c091c5..e9ebfd00 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt @@ -1,5 +1,7 @@ package com.brainwallet.ui.screens.main +import android.graphics.Bitmap +import android.view.ViewGroup import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.background @@ -11,7 +13,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.brainwallet.navigation.OnNavigate @@ -32,7 +33,7 @@ fun WebModalScreen( LaunchedEffect(Unit) { AnalyticsManager.logCustomEvent(eventString) } - val context = LocalContext.current + Card( modifier = modifier .fillMaxSize() @@ -40,13 +41,35 @@ fun WebModalScreen( shape = RoundedCornerShape(18.dp), ) { AndroidView( + modifier = Modifier.fillMaxSize(), factory = { context -> WebView(context).apply { - webViewClient = WebViewClient() - loadUrl(url) + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + setBackgroundColor(0) + + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.useWideViewPort = true + + webViewClient = object : WebViewClient() { + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + } + } } }, - modifier = Modifier.fillMaxSize() + update = { + it.loadUrl(url) + } ) } }