Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ dependencies {
implementation(libs.charts)
implementation(libs.haze)
implementation(libs.haze.materials)
// Compose Navigation
implementation(libs.navigation.compose)
androidTestImplementation(libs.navigation.testing)
// Navigation
implementation(libs.navigation.runtime)
implementation(libs.navigation.ui)
implementation(libs.hilt.navigation.compose)
// Hilt - DI
implementation(libs.hilt.android)
Expand Down
13 changes: 5 additions & 8 deletions app/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
<ID>ComplexCondition:MapWebViewClient.kt$MapWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND</ID>
<ID>ComplexCondition:ShopWebViewClient.kt$ShopWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND</ID>
<ID>CyclomaticComplexMethod:ActivityListGrouped.kt$private fun groupActivityItems(activityItems: List&lt;Activity&gt;): List&lt;Any&gt;</ID>
<ID>CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (String) -&gt; Unit, testTag: String, )</ID>
<ID>CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (Activity) -&gt; Unit, testTag: String, )</ID>
<ID>CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private fun observeSendEvents()</ID>
<ID>CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private suspend fun handleSanityChecks(amountSats: ULong)</ID>
<ID>CyclomaticComplexMethod:BlocktankRegtestScreen.kt$@Composable fun BlocktankRegtestScreen( navController: NavController, viewModel: BlocktankRegtestViewModel = hiltViewModel(), )</ID>
<ID>CyclomaticComplexMethod:BlocktankRegtestScreen.kt$@Composable private fun BlocktankRegtestContent( onBack: () -&gt; Unit, viewModel: BlocktankRegtestViewModel, )</ID>
<ID>CyclomaticComplexMethod:ConfirmMnemonicScreen.kt$@Composable fun ConfirmMnemonicScreen( uiState: BackupContract.UiState, onContinue: () -&gt; Unit, onBack: () -&gt; Unit, )</ID>
<ID>CyclomaticComplexMethod:HealthRepo.kt$HealthRepo$private fun collectState()</ID>
<ID>CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, rootNavController: NavController, walletNavController: NavHostController, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), )</ID>
<ID>CyclomaticComplexMethod:SendSheet.kt$@Composable fun SendSheet( appViewModel: AppViewModel, walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, )</ID>
<ID>CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, navigator: Navigator, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), )</ID>
<ID>CyclomaticComplexMethod:SettingsButtonRow.kt$@Composable fun SettingsButtonRow( title: String, modifier: Modifier = Modifier, subtitle: String? = null, value: SettingsButtonValue = SettingsButtonValue.None, description: String? = null, iconRes: Int? = null, iconTint: Color = Color.Unspecified, iconSize: Dp = 32.dp, maxLinesSubtitle: Int = Int.MAX_VALUE, enabled: Boolean = true, loading: Boolean = false, onClick: () -&gt; Unit, )</ID>
<ID>CyclomaticComplexMethod:Slider.kt$@Composable fun StepSlider( value: Int, steps: List&lt;Int&gt;, onValueChange: (Int) -&gt; Unit, modifier: Modifier = Modifier, )</ID>
<ID>DestructuringDeclarationWithTooManyEntries:ActivityRow.kt$val (_, _, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current</ID>
Expand All @@ -34,7 +33,6 @@
<ID>EnumNaming:BlocktankNotificationType.kt$BlocktankNotificationType$wakeToTimeout</ID>
<ID>ForbiddenComment:ActivityDetailScreen.kt$/* TODO: Implement assign functionality */</ID>
<ID>ForbiddenComment:BoostTransactionViewModel.kt$BoostTransactionUiState$// TODO: Implement dynamic time estimation</ID>
<ID>ForbiddenComment:ContentView.kt$// TODO: display as sheet</ID>
<ID>ForbiddenComment:ExternalNodeViewModel.kt$ExternalNodeViewModel$// TODO: pass customFeeRate to ldk-node when supported</ID>
<ID>ForbiddenComment:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$// TODO: sort channels to get consistent index; node.listChannels returns a list in random order</ID>
<ID>ForbiddenComment:LightningService.kt$LightningService$// TODO: cleanup sensitive data after implementing a `SecureString` value holder for Keychain return values</ID>
Expand All @@ -43,12 +41,11 @@
<ID>FunctionOnlyReturningConstant:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun isReady(): Boolean</ID>
<ID>ImplicitDefaultLocale:BlocksService.kt$BlocksService$String.format("%.2f", blockInfo.difficulty / 1_000_000_000_000.0)</ID>
<ID>ImplicitDefaultLocale:PriceService.kt$PriceService$String.format("%.2f", price)</ID>
<ID>ImportOrdering:TransferEntries.kt$import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import to.bitkit.models.Toast import to.bitkit.ui.nav.Navigator import to.bitkit.ui.nav.Routes import to.bitkit.ui.screens.transfer.FundingAdvancedScreen import to.bitkit.ui.screens.transfer.FundingScreen import to.bitkit.ui.screens.transfer.LiquidityScreen import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen import to.bitkit.ui.screens.transfer.SavingsConfirmScreen import to.bitkit.ui.screens.transfer.SavingsIntroScreen import to.bitkit.ui.screens.transfer.SavingsProgressScreen import to.bitkit.ui.screens.transfer.SettingUpScreen import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen import to.bitkit.ui.screens.transfer.SpendingAmountScreen import to.bitkit.ui.screens.transfer.SpendingConfirmScreen import to.bitkit.ui.screens.transfer.SpendingIntroScreen import to.bitkit.ui.screens.transfer.TransferIntroScreen import to.bitkit.ui.screens.transfer.external.ExternalAmountScreen import to.bitkit.ui.screens.transfer.external.ExternalConfirmScreen import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen import to.bitkit.ui.screens.scanner.QrScanningScreen import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel</ID>
<ID>InstanceOfCheckForException:LightningService.kt$LightningService$e is NodeException</ID>
<ID>LargeClass:AppViewModel.kt$AppViewModel : ViewModel</ID>
<ID>LargeClass:LightningRepo.kt$LightningRepo</ID>
<ID>LongMethod:AppViewModel.kt$AppViewModel$private suspend fun proceedWithPayment()</ID>
<ID>LongMethod:ContentView.kt$@Suppress("LongParameterList") private fun NavGraphBuilder.home( walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, settingsViewModel: SettingsViewModel, navController: NavHostController, drawerState: DrawerState, )</ID>
<ID>LongMethod:ContentView.kt$private fun NavGraphBuilder.widgets( navController: NavHostController, settingsViewModel: SettingsViewModel, currencyViewModel: CurrencyViewModel, )</ID>
<ID>LongMethod:CoreService.kt$ActivityService$suspend fun generateRandomTestData(count: Int = 100)</ID>
<ID>LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceed: () -&gt; Unit, onAuthFailed: (() -&gt; Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -&gt; Unit), )</ID>
Expand Down Expand Up @@ -112,6 +109,7 @@
<ID>MaxLineLength:HeadlineCard.kt$headline = "How Bitcoin changed El Salvador in more ways a big headline to test the text overflooooooow"</ID>
<ID>MaxLineLength:LightningBalance.kt$is LightningBalance.ClaimableAwaitingConfirmations -&gt; "Claimable Awaiting Confirmations (Height: $confirmationHeight)"</ID>
<ID>MaxLineLength:LightningConnectionsScreen.kt$if (showClosed) R.string.lightning__conn_closed_hide else R.string.lightning__conn_closed_show</ID>
<ID>MaxLineLength:LightningRepo.kt$LightningRepo$"Cannot execute $operationName: Node is ${_lightningState.value.nodeLifecycleState} and not starting"</ID>
<ID>MaxLineLength:LightningRepo.kt$LightningRepo$"accelerateByCpfp error originalTxId: $originalTxId, satsPerVByte: $satsPerVByte destinationAddress: $destinationAddress"</ID>
<ID>MaxLineLength:LightningRepo.kt$LightningRepo$"accelerateByCpfp success, newDestinationTxId: $newDestinationTxId originalTxId: $originalTxId, satsPerVByte: $satsPerVByte destinationAddress: $destinationAddress"</ID>
<ID>MaxLineLength:LightningRepo.kt$LightningRepo$"bumpFeeByRbf success, replacementTxId: $replacementTxId originalTxId: $originalTxId, satsPerVByte: $satsPerVByte"</ID>
Expand Down Expand Up @@ -190,7 +188,6 @@
<ID>TooManyFunctions:BackupNavSheetViewModel.kt$BackupNavSheetViewModel : ViewModel</ID>
<ID>TooManyFunctions:BlocktankRepo.kt$BlocktankRepo</ID>
<ID>TooManyFunctions:CacheStore.kt$CacheStore</ID>
<ID>TooManyFunctions:ContentView.kt$to.bitkit.ui.ContentView.kt</ID>
<ID>TooManyFunctions:CoreService.kt$ActivityService</ID>
<ID>TooManyFunctions:CoreService.kt$BlocktankService</ID>
<ID>TooManyFunctions:DevSettingsViewModel.kt$DevSettingsViewModel : ViewModel</ID>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class VssBackupClient @Inject constructor(
}
}

companion object Companion {
companion object {
private const val TAG = "VssBackupClient"
}
}
151 changes: 87 additions & 64 deletions app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package to.bitkit.repositories

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import to.bitkit.data.SettingsStore
Expand All @@ -30,6 +33,7 @@ import to.bitkit.models.widget.HeadlinePreferences
import to.bitkit.models.widget.PricePreferences
import to.bitkit.models.widget.WeatherPreferences
import to.bitkit.utils.Logger
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -44,9 +48,8 @@ class WidgetsRepo @Inject constructor(
private val widgetsStore: WidgetsStore,
private val settingsStore: SettingsStore,
) {
// TODO Only refresh in loop widgets displayed in the Home
// TODO Perform a refresh when the preview screen is displayed
private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob())
private val widgetJobs = ConcurrentHashMap<WidgetType, Job>()

val widgetsDataFlow = widgetsStore.data
val showWidgetTitles = settingsStore.data.map { it.showWidgetTitles }
Expand All @@ -63,7 +66,86 @@ class WidgetsRepo @Inject constructor(
val refreshStates: StateFlow<Map<WidgetType, Boolean>> = _refreshStates.asStateFlow()

init {
startPeriodicUpdates()
observeWidgetStateChanges()
}

private fun observeWidgetStateChanges() {
repoScope.launch {
widgetsDataFlow
.map { it.widgets.map { widget -> widget.type }.toSet() }
.distinctUntilChanged()
.collect { enabledWidgetTypes ->
updateWidgetJobs(enabledWidgetTypes)
}
}
}

private fun updateWidgetJobs(enabledWidgetTypes: Set<WidgetType>) {
val widgetTypesWithServices = WidgetType.entries.filter {
it != WidgetType.CALCULATOR
}

widgetTypesWithServices.forEach { widgetType ->
val isEnabled = widgetType in enabledWidgetTypes
val hasRunningJob = widgetJobs.containsKey(widgetType) &&
widgetJobs[widgetType]?.isActive == true

when {
isEnabled && !hasRunningJob -> startWidgetRefresh(widgetType)
!isEnabled && hasRunningJob -> stopWidgetRefresh(widgetType)
}
}
}

private fun startWidgetRefresh(widgetType: WidgetType) {
stopWidgetRefresh(widgetType)

val job = when (widgetType) {
WidgetType.NEWS -> repoScope.launch {
while (isActive) {
updateWidget(newsService) { widgetsStore.updateArticles(it) }
delay(newsService.refreshInterval)
}
}

WidgetType.FACTS -> repoScope.launch {
while (isActive) {
updateWidget(factsService) { widgetsStore.updateFacts(it) }
delay(factsService.refreshInterval)
}
}

WidgetType.BLOCK -> repoScope.launch {
while (isActive) {
updateWidget(blocksService) { widgetsStore.updateBlock(it) }
delay(blocksService.refreshInterval)
}
}

WidgetType.WEATHER -> repoScope.launch {
while (isActive) {
updateWidget(weatherService) { widgetsStore.updateWeather(it) }
delay(weatherService.refreshInterval)
}
}

WidgetType.PRICE -> repoScope.launch {
while (isActive) {
updateWidget(priceService) { widgetsStore.updatePrice(it) }
delay(priceService.refreshInterval)
}
}

WidgetType.CALCULATOR -> throw NotImplementedError("Calculator widget doesn't need a service")
}

widgetJobs[widgetType] = job
}

private fun stopWidgetRefresh(widgetType: WidgetType) {
widgetJobs[widgetType]?.cancel()
widgetJobs.remove(widgetType)
Logger.verbose("Stopped refresh coroutine for $widgetType", context = TAG)
}

suspend fun addWidget(type: WidgetType) = withContext(bgDispatcher) { widgetsStore.addWidget(type) }
Expand Down Expand Up @@ -96,56 +178,18 @@ class WidgetsRepo @Inject constructor(

suspend fun fetchAllPeriods() = withContext(bgDispatcher) { priceService.fetchAllPeriods() }

/**
* Start periodic updates for all widgets
*/
private fun startPeriodicUpdates() {
startPeriodicUpdate(newsService) { articles ->
widgetsStore.updateArticles(articles)
}
startPeriodicUpdate(factsService) { facts ->
widgetsStore.updateFacts(facts)
}
startPeriodicUpdate(blocksService) { block ->
widgetsStore.updateBlock(block)
}
startPeriodicUpdate(weatherService) { weather ->
widgetsStore.updateWeather(weather)
}
startPeriodicUpdate(priceService) { price ->
widgetsStore.updatePrice(price)
}
}

/**
* Generic method to start periodic updates for any widget service
*/
private fun <T> startPeriodicUpdate(
service: WidgetService<T>,
updateStore: suspend (T) -> Unit
) {
repoScope.launch {
while (true) {
updateWidget(service, updateStore)
delay(service.refreshInterval)
}
}
}

/**
* Update a specific widget type
*/
private suspend fun <T> updateWidget(
service: WidgetService<T>,
updateStore: suspend (T) -> Unit
updateStore: suspend (T) -> Unit,
) {
val widgetType = service.widgetType
_refreshStates.update { it + (widgetType to true) }

service.fetchData()
.onSuccess { data ->
updateStore(data)
Logger.verbose("Updated $widgetType widget successfully")
Logger.verbose("Updated $widgetType widget successfully", context = TAG)
}
.onFailure { e ->
Logger.verbose("Failed to update $widgetType widget", e = e, context = TAG)
Expand All @@ -154,27 +198,6 @@ class WidgetsRepo @Inject constructor(
_refreshStates.update { it + (widgetType to false) }
}

/**
* Manually refresh all widgets
*/
suspend fun refreshAllWidgets(): Result<Unit> = runCatching {
updateWidget(newsService) { articles ->
widgetsStore.updateArticles(articles)
}
updateWidget(factsService) { facts ->
widgetsStore.updateFacts(facts)
}
updateWidget(blocksService) { block ->
widgetsStore.updateBlock(block)
}
updateWidget(weatherService) { weather ->
widgetsStore.updateWeather(weather)
}
updateWidget(priceService) { price ->
widgetsStore.updatePrice(price)
}
}

suspend fun refreshEnabledWidgets() = withContext(bgDispatcher) {
widgetsDataFlow.first().widgets.forEach {
refreshWidget(it.type)
Expand Down
Loading
Loading