From ed0374c69e5ab5e78c216d891def6573b386e9cf Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 28 Feb 2024 07:56:50 -0800 Subject: [PATCH 1/4] feat: add more mixing progress indicators --- .../wallet/common/ui/PaymentHeaderView.kt | 4 ++ .../animated_circular_progress_indictator.xml | 6 +++ .../drawable/circular_progress_indicator.xml | 10 +++++ common/src/main/res/values/strings.xml | 2 + wallet/res/layout/activity_settings.xml | 44 ++++++++++++++++--- wallet/res/layout/mixing_status_pane.xml | 36 ++++++++++++++- wallet/res/values/strings.xml | 4 +- .../schildbach/wallet/ui/SettingsActivity.kt | 29 +++++++++--- .../wallet/ui/main/MainViewModel.kt | 2 + .../wallet/ui/main/WalletFragment.kt | 20 +++++++++ .../wallet/ui/more/SettingsViewModel.kt | 4 ++ .../wallet/ui/send/SendCoinsFragment.kt | 7 +++ .../wallet/ui/send/SendCoinsViewModel.kt | 10 +++-- 13 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 common/src/main/res/drawable/animated_circular_progress_indictator.xml create mode 100644 common/src/main/res/drawable/circular_progress_indicator.xml diff --git a/common/src/main/java/org/dash/wallet/common/ui/PaymentHeaderView.kt b/common/src/main/java/org/dash/wallet/common/ui/PaymentHeaderView.kt index 8a976f4b85..5b1db44488 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/PaymentHeaderView.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/PaymentHeaderView.kt @@ -66,6 +66,10 @@ class PaymentHeaderView @JvmOverloads constructor( binding.paymentAddressViewTitle.text = title } + fun setBalanceTitle(title: String) { + binding.paymentAddressViewBalanceTitle.text = title + } + fun setProposition(title: String) { binding.paymentAddressViewProposition.text = title } diff --git a/common/src/main/res/drawable/animated_circular_progress_indictator.xml b/common/src/main/res/drawable/animated_circular_progress_indictator.xml new file mode 100644 index 0000000000..5ab3748d3e --- /dev/null +++ b/common/src/main/res/drawable/animated_circular_progress_indictator.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/common/src/main/res/drawable/circular_progress_indicator.xml b/common/src/main/res/drawable/circular_progress_indicator.xml new file mode 100644 index 0000000000..2fcda7b5d7 --- /dev/null +++ b/common/src/main/res/drawable/circular_progress_indicator.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index f7d13355d1..2a32d0e56c 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -106,4 +106,6 @@ available Not available Log In + + %d%% \ No newline at end of file diff --git a/wallet/res/layout/activity_settings.xml b/wallet/res/layout/activity_settings.xml index de95f609b4..44176a9cc9 100644 --- a/wallet/res/layout/activity_settings.xml +++ b/wallet/res/layout/activity_settings.xml @@ -130,29 +130,61 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> + + + + + + + app:layout_constraintBottom_toBottomOf="@id/coinjoin_subtitle" + app:layout_constraintEnd_toEndOf="parent" /> + + + + + + + Start Mixing Stop Mixing Mixing - Mixing ยท %1$s of %2$s + Mixing Paused + %s (%d%%) %s of %s %1$s of %2$s Fully Mixed Are you sure you want to change the privacy level? Are you sure you want to stop mixing? Any funds that have been mixed will be combined with your unmixed funds + Mixed balance: diff --git a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt index 4fc3ef36b6..13999677c1 100644 --- a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt @@ -23,6 +23,7 @@ import android.net.Uri import android.os.Bundle import android.provider.Settings import androidx.activity.viewModels +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope @@ -159,17 +160,35 @@ class SettingsActivity : LockScreenActivity() { viewModel.coinJoinMixingMode.observe(this) { mode -> if (mode == CoinJoinMode.NONE) { binding.coinjoinSubtitle.text = getText(R.string.turned_off) + binding.coinjoinSubtitleIcon.isVisible = false + binding.progressBar.isVisible = false + binding.balance.isVisible = false } else { if (viewModel.coinJoinMixingStatus == MixingStatus.FINISHED) { binding.coinjoinSubtitle.text = getString(R.string.coinjoin_progress_finished) binding.coinjoinSubtitleIcon.isVisible = false + binding.progressBar.isVisible = false } else { - binding.coinjoinSubtitle.text = getString( - R.string.coinjoin_progress, - viewModel.mixedBalance, - viewModel.walletBalance - ) + @StringRes val statusId = when(viewModel.coinJoinMixingStatus) { + MixingStatus.MIXING -> R.string.coinjoin_mixing + MixingStatus.PAUSED -> R.string.coinjoin_paused + MixingStatus.FINISHED -> R.string.coinjoin_progress_finished + else -> R.string.error + } + + binding.coinjoinSubtitle.text = getString(statusId) +// getString( +// R.string.coinjoin_progress, +// getString(statusId), +// viewModel.mixingProgress.toInt(), +// viewModel.mixedBalance, +// viewModel.walletBalance +// ) binding.coinjoinSubtitleIcon.isVisible = true + binding.progressBar.isVisible = true + binding.coinjoinProgress.text = getString(R.string.percent, viewModel.mixingProgress.toInt()) + binding.balance.isVisible = true + binding.balance.text = getString(R.string.coinjoin_progress_balance, viewModel.mixedBalance, viewModel.walletBalance) } } } diff --git a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt index 5ea27f5ae8..b60cd56ef5 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt @@ -214,6 +214,8 @@ class MainViewModel @Inject constructor( get() = coinJoinService.observeMixingState() val mixingProgress: Flow get() = coinJoinService.observeMixingProgress() + val mixingSessions: Flow + get() = coinJoinService.observeActiveSessions() var decimalFormat: DecimalFormat = DecimalFormat("0.000") val walletBalance: String diff --git a/wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt b/wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt index 8b7a904b94..83cb069163 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt @@ -167,6 +167,7 @@ class WalletFragment : Fragment(R.layout.home_content) { viewModel.mixedBalance, viewModel.walletBalance ) + mixingBinding.mixingPercent.text = getString(R.string.percent, progress.toInt()) mixingBinding.mixingProgress.progress = progress.toInt() } @@ -177,6 +178,25 @@ class WalletFragment : Fragment(R.layout.home_content) { MixingStatus.FINISHED -> false else -> true } + when (mixingState) { + MixingStatus.MIXING -> { + mixingBinding.mixingMode.text = getString(R.string.coinjoin_mixing) + mixingBinding.progressBar.isVisible = true + } + MixingStatus.PAUSED -> { + mixingBinding.mixingMode.text = getString(R.string.coinjoin_paused) + mixingBinding.progressBar.isVisible = false + } + else -> { + mixingBinding.mixingMode.text = getString(R.string.error) + mixingBinding.progressBar.isVisible = false + } + } + } + + viewModel.mixingSessions.observe(viewLifecycleOwner) { + val activeSessionsText = ".".repeat(it) + mixingBinding.mixingSessions.text = activeSessionsText } } diff --git a/wallet/src/de/schildbach/wallet/ui/more/SettingsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/more/SettingsViewModel.kt index 50015cacb6..4df2bfbd03 100644 --- a/wallet/src/de/schildbach/wallet/ui/more/SettingsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/more/SettingsViewModel.kt @@ -52,12 +52,16 @@ class SettingsViewModel @Inject constructor( val voteDashPayIsEnabled = walletUIConfig.observe(WalletUIConfig.VOTE_DASH_PAY_ENABLED) val coinJoinMixingMode: Flow get() = coinJoinConfig.observeMode() + var mixingProgress: Double = 0.0 var coinJoinMixingStatus: MixingStatus = MixingStatus.NOT_STARTED init { coinJoinService.observeMixingState() .onEach { coinJoinMixingStatus = it } .launchIn(viewModelScope) + coinJoinService.observeMixingProgress() + .onEach { mixingProgress = it } + .launchIn(viewModelScope) } var decimalFormat: DecimalFormat = DecimalFormat("0.000") diff --git a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt index f7f1de2f9e..971a75c116 100644 --- a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt @@ -32,6 +32,7 @@ import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.integration.android.BitcoinIntegration +import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.ui.dashpay.DashPayViewModel import de.schildbach.wallet.ui.LockScreenActivity import de.schildbach.wallet.ui.transactions.TransactionResultActivity @@ -53,6 +54,7 @@ import org.dash.wallet.common.ui.dialogs.MinimumBalanceDialog import org.dash.wallet.common.ui.enter_amount.EnterAmountFragment import org.dash.wallet.common.ui.enter_amount.EnterAmountViewModel import org.dash.wallet.common.ui.viewBinding +import org.dash.wallet.common.util.observe import org.dash.wallet.common.util.toFormattedString import org.slf4j.LoggerFactory import javax.inject.Inject @@ -156,6 +158,11 @@ class SendCoinsFragment: Fragment(R.layout.send_coins_fragment) { } updateView() } + viewModel.coinJoinMode.observe(viewLifecycleOwner) { mode -> + if (mode != CoinJoinMode.NONE) { + binding.paymentHeader.setBalanceTitle(getString(R.string.coinjoin_mixed_balance)) + } + } enterAmountViewModel.amount.observe(viewLifecycleOwner) { viewModel.currentAmount = it } enterAmountViewModel.dashToFiatDirection.observe(viewLifecycleOwner) { viewModel.isDashToFiatPreferred = it } diff --git a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt index eaded63b88..fddf3c86db 100644 --- a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt @@ -34,6 +34,8 @@ import de.schildbach.wallet.payments.SendCoinsTaskRunner import de.schildbach.wallet.security.BiometricHelper import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.ui.dashpay.PlatformRepo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest @@ -123,7 +125,9 @@ class SendCoinsViewModel @Inject constructor( val contactData: LiveData get() = _contactData - private var coinJoinMode: CoinJoinMode = CoinJoinMode.NONE + private var _coinJoinMode = MutableStateFlow(CoinJoinMode.NONE) + val coinJoinMode: Flow + get() = _coinJoinMode init { blockchainStateDao.observeState() @@ -135,7 +139,7 @@ class SendCoinsViewModel @Inject constructor( coinJoinConfig.observeMode() .map { mode -> - coinJoinMode = mode + _coinJoinMode.value = mode if (mode == CoinJoinMode.NONE) { MaxOutputAmountCoinSelector() } else { @@ -299,7 +303,7 @@ class SendCoinsViewModel @Inject constructor( dryrunSendRequest = sendRequest _dryRunSuccessful.value = true } catch (ex: Exception) { - dryRunException = if (ex is InsufficientMoneyException && coinJoinMode != CoinJoinMode.NONE && !currentAmount.isGreaterThan(wallet.getBalance(MaxOutputAmountCoinSelector()))) { + dryRunException = if (ex is InsufficientMoneyException && _coinJoinMode.value != CoinJoinMode.NONE && !currentAmount.isGreaterThan(wallet.getBalance(MaxOutputAmountCoinSelector()))) { InsufficientCoinJoinMoneyException(ex) } else { ex From dd4f59f9919dc7abbd4a3b592a0720211b63f739 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 28 Feb 2024 07:57:12 -0800 Subject: [PATCH 2/4] fix: improve TimeSkew function --- .../de/schildbach/wallet/util/TimeUtils.kt | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/util/TimeUtils.kt b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt index f3238fbe82..de39a15b09 100644 --- a/wallet/src/de/schildbach/wallet/util/TimeUtils.kt +++ b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt @@ -2,12 +2,14 @@ package de.schildbach.wallet.util import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.head +import org.slf4j.LoggerFactory import java.net.DatagramPacket import java.net.DatagramSocket import java.net.InetAddress import java.net.SocketTimeoutException -import kotlin.math.abs +import kotlin.jvm.Throws +private val log = LoggerFactory.getLogger("TimeUtils") private fun queryNtpTime(server: String): Long? { try { val address = InetAddress.getByName(server) @@ -42,17 +44,32 @@ private fun queryNtpTime(server: String): Long? { return null } +@Throws(NullPointerException::class) suspend fun getTimeSkew(): Long { var networkTime: Long? = null - try { - networkTime = queryNtpTime("pool.ntp.org") - } catch (e: SocketTimeoutException) { - // swallow, the next block will use alternate method + var timeSource = "NTP" + + val networkTimes = arrayListOf() + for (i in 0..3) { + try { + val time = queryNtpTime("pool.ntp.org") + if (time != null && time > 0) { networkTimes.add(time) } + } catch (e: SocketTimeoutException) { + // swallow + } + } + networkTimes.sort() + when (networkTimes.size) { + 3 -> networkTime = networkTimes[2] + 2 -> networkTime = (networkTimes[0] + networkTimes[1]) / 2 + else -> { } } + if (networkTime == null) { try { val result = Constants.HTTP_CLIENT.head("https://www.dash.org/") networkTime = result.headers.getDate("date")?.time + timeSource = "dash.org" } catch (e: Exception) { // swallow } @@ -60,12 +77,16 @@ suspend fun getTimeSkew(): Long { try { val result = Constants.HTTP_CLIENT.head("https://insight.dash.org/insight") networkTime = result.headers.getDate("date")?.time + timeSource = "insight" } catch (e: Exception) { // swallow } } + log.info("timeskew: network time is $networkTime") requireNotNull(networkTime) } + val systemTimeMillis = System.currentTimeMillis() + log.info("timeskew: $systemTimeMillis-$networkTime = ${systemTimeMillis - networkTime}; source: $timeSource") return systemTimeMillis - networkTime } From 70c1ed375ff9b70f9bad994eecb82abe682a96d9 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 28 Feb 2024 07:57:41 -0800 Subject: [PATCH 3/4] fix: keep mixing process alive --- wallet/build.gradle | 4 +- .../wallet/service/BlockchainServiceImpl.java | 70 ++++++++++++----- .../wallet/service/CoinJoinService.kt | 75 ++++++++++++------- .../wallet/service/ForegroundService.kt | 7 ++ 4 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 wallet/src/de/schildbach/wallet/service/ForegroundService.kt diff --git a/wallet/build.gradle b/wallet/build.gradle index 354e2efc4f..ecfaed53bb 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -214,8 +214,8 @@ android { compileSdk 33 minSdkVersion 23 targetSdkVersion 33 - versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 90000 - versionName project.hasProperty('versionName') ? project.property('versionName') : "5.3-dashpay" + versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 90002 + versionName project.hasProperty('versionName') ? project.property('versionName') : "5.4-dashpay" multiDexEnabled true generatedDensities = ['hdpi', 'xhdpi'] vectorDrawables.useSupportLibrary = true diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java index cfbec18335..b4182e6366 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java @@ -145,9 +145,6 @@ import de.schildbach.wallet.util.CrashReporter; import de.schildbach.wallet.util.ThrottlingWalletChangeListener; import de.schildbach.wallet_test.R; -import kotlin.Unit; -import kotlin.coroutines.Continuation; -import kotlinx.coroutines.flow.FlowCollector; import static org.dash.wallet.common.util.Constants.PREFIX_ALMOST_EQUAL_TO; @@ -224,7 +221,8 @@ public class BlockchainServiceImpl extends LifecycleService implements Blockchai private Executor executor = Executors.newSingleThreadExecutor(); private int syncPercentage = 0; // 0 to 100% private MixingStatus mixingStatus = MixingStatus.NOT_STARTED; - private boolean isForegroundService = false; + private Double mixingProgress = 0.0; + private ForegroundService foregroundService = ForegroundService.NONE; // Risk Analyser for Transactions that is PeerGroup Aware AllowLockTimeRiskAnalysis.Analyzer riskAnalyzer; @@ -1042,25 +1040,46 @@ public void onCreate() { updateAppWidget(); FlowExtKt.observe(blockchainStateDao.observeState(), this, (blockchainState, continuation) -> { - handleBlockchainStateNotification((BlockchainState) blockchainState, mixingStatus); + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress); return null; }); registerCrowdNodeConfirmedAddressFilter(); FlowExtKt.observe(coinJoinService.observeMixingState(), this, (mixingStatus, continuation) -> { - handleBlockchainStateNotification(blockchainState, mixingStatus); + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress); + return null; + }); + + FlowExtKt.observe(coinJoinService.observeMixingProgress(), this, (mixingProgress, continuation) -> { + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress); return null; }); } - private Notification createCoinJoinNotification(Coin mixedBalance, Coin totalBalance) { + private Notification createCoinJoinNotification() { + Coin mixedBalance = ((WalletEx)application.getWallet()).getCoinJoinBalance(); + Coin totalBalance = application.getWallet().getBalance(); Intent notificationIntent = OnboardingActivity.createIntent(this); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); DecimalFormat decimalFormat = new DecimalFormat("0.000"); + int statusStringId = R.string.error; + switch(mixingStatus) { + case MIXING: + statusStringId = R.string.coinjoin_mixing; + break; + case PAUSED: + statusStringId = R.string.coinjoin_paused; + break; + case FINISHED: + statusStringId = R.string.coinjoin_progress_finished; + break; + } final String message = getString( R.string.coinjoin_progress, + getString(statusStringId), + mixingProgress.intValue(), decimalFormat.format(MonetaryExtKt.toBigDecimal(mixedBalance)), decimalFormat.format(MonetaryExtKt.toBigDecimal(totalBalance)) ); @@ -1156,7 +1175,15 @@ private void startForeground() { //preventing it from being killed in Android 26 or later Notification notification = createNetworkSyncNotification(null); startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); - isForegroundService = true; + foregroundService = ForegroundService.BLOCKCHAIN_SYNC; + } + + private void startForegroundCoinJoin() { + // Shows ongoing notification promoting service to foreground service and + // preventing it from being killed in Android 26 or later + Notification notification = createCoinJoinNotification(); + startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); + foregroundService = ForegroundService.COINJOIN_MIXING; } @Override @@ -1308,12 +1335,13 @@ private void broadcastPeerState(final int numPeers) { LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } - private void handleBlockchainStateNotification(BlockchainState blockchainState, MixingStatus mixingStatus) { + private void handleBlockchainStateNotification(BlockchainState blockchainState, MixingStatus mixingStatus, double mixingProgress) { // send this out for the Network Monitor, other activities observe the database final Intent broadcast = new Intent(ACTION_BLOCKCHAIN_STATE); broadcast.setPackage(getPackageName()); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - + log.info("handle blockchain state notification: {}, {}", foregroundService, mixingStatus); + this.mixingProgress = mixingProgress; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && blockchainState != null && blockchainState.getBestChainDate() != null) { //Handle Ongoing notification state @@ -1321,21 +1349,25 @@ private void handleBlockchainStateNotification(BlockchainState blockchainState, if (!syncing && blockchainState.getBestChainHeight() == config.getBestChainHeightEver() && mixingStatus != MixingStatus.MIXING) { //Remove ongoing notification if blockchain sync finished stopForeground(true); - isForegroundService = false; + foregroundService = ForegroundService.NONE; nm.cancel(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC); } else if (blockchainState.getReplaying() || syncing) { //Shows ongoing notification when synchronizing the blockchain Notification notification = createNetworkSyncNotification(blockchainState); nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); - } else if (mixingStatus == MixingStatus.MIXING) { - Notification notification = createCoinJoinNotification( - ((WalletEx)application.getWallet()).getCoinJoinBalance(), - application.getWallet().getBalance() - ); - if (isForegroundService) { - nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); + } else if (mixingStatus == MixingStatus.MIXING || mixingStatus == MixingStatus.PAUSED) { + log.info("foreground service: {}", foregroundService); + if (foregroundService == ForegroundService.NONE) { + log.info("foreground service not active, create notification"); + startForegroundCoinJoin(); + //Notification notification = createCoinJoinNotification(); + //nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); + foregroundService = ForegroundService.COINJOIN_MIXING; } else { - startForeground(); + log.info("foreground service active, update notification"); + Notification notification = createCoinJoinNotification(); + //nm.cancel(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC); + nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); } } } diff --git a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt index daa05a415b..30e1b56796 100644 --- a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt +++ b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt @@ -51,6 +51,7 @@ import org.bitcoinj.coinjoin.callbacks.RequestKeyParameter import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener import org.bitcoinj.coinjoin.listeners.MixingCompleteListener import org.bitcoinj.coinjoin.listeners.SessionCompleteListener +import org.bitcoinj.coinjoin.listeners.SessionStartedListener import org.bitcoinj.coinjoin.progress.MixingProgressTracker import org.bitcoinj.coinjoin.utils.CoinJoinManager import org.bitcoinj.coinjoin.utils.CoinJoinTransactionType @@ -86,8 +87,10 @@ enum class CoinJoinMode { * Monitor the status of the CoinJoin Mixing Service */ interface CoinJoinService { + fun observeActiveSessions(): Flow suspend fun getMixingState(): MixingStatus fun observeMixingState(): Flow + suspend fun getMixingProgress(): Double fun observeMixingProgress(): Flow fun updateTimeSkew(timeSkew: Long) } @@ -115,7 +118,7 @@ class CoinJoinMixingService @Inject constructor( companion object { val log: Logger = LoggerFactory.getLogger(CoinJoinMixingService::class.java) const val DEFAULT_MULTISESSION = false // for stability, need to investigate - const val DEFAULT_ROUNDS = 1 + const val DEFAULT_ROUNDS = 4 const val DEFAULT_SESSIONS = 4 const val DEFAULT_DENOMINATIONS_GOAL = 50 const val DEFAULT_DENOMINATIONS_HARDCAP = 300 @@ -138,6 +141,7 @@ class CoinJoinMixingService @Inject constructor( private var mixingCompleteListeners: ArrayList = arrayListOf() private var sessionCompleteListeners: ArrayList = arrayListOf() + private var sessionStartedListeners: ArrayList = arrayListOf() var mode: CoinJoinMode = CoinJoinMode.NONE private val _mixingState = MutableStateFlow(MixingStatus.NOT_STARTED) @@ -146,9 +150,8 @@ class CoinJoinMixingService @Inject constructor( override suspend fun getMixingState(): MixingStatus = _mixingState.value override fun observeMixingState(): Flow = _mixingState - private val coroutineScope = CoroutineScope( - Executors.newFixedThreadPool(2, ContextPropagatingThreadFactory("coinjoin-pool")).asCoroutineDispatcher() - ) + private val executor = Executors.newFixedThreadPool(2, ContextPropagatingThreadFactory("coinjoin-pool")) + private val coroutineScope = CoroutineScope(executor.asCoroutineDispatcher()) private val uiCoroutineScope = CoroutineScope(Dispatchers.Main) @@ -159,6 +162,8 @@ class CoinJoinMixingService @Inject constructor( private var isSynced = false private var hasAnonymizableBalance: Boolean = false private var timeSkew: Long = 0L + private var activeSessions = 0 + private val activeSessionsFlow = MutableStateFlow(0) // https://stackoverflow.com/questions/55421710/how-to-suspend-kotlin-coroutine-until-notified private val updateMutex = Mutex(locked = false) @@ -199,6 +204,7 @@ class CoinJoinMixingService @Inject constructor( .onEach { blockChainState -> val isSynced = blockChainState.isSynced() if (isSynced != this.isSynced) { + val networkStatus = blockchainStateProvider.getNetworkStatus() updateState(config.getMode(), timeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain) } // this will trigger mixing as new blocks are mined and received tx's are confirmed @@ -304,7 +310,7 @@ class CoinJoinMixingService @Inject constructor( log.info( "coinjoin-new-state: $mode, $timeSkew ms, $hasAnonymizableBalance, $networkStatus, synced: $isSynced, ${blockChain != null}" ) - log.info("coinjoin-Current timeskew: ${getCurrentTimeSkew()}") + // log.info("coinjoin-Current timeskew: ${getCurrentTimeSkew()}") this.networkStatus = networkStatus this.hasAnonymizableBalance = hasAnonymizableBalance this.isSynced = isSynced @@ -370,8 +376,8 @@ class CoinJoinMixingService @Inject constructor( CoinJoinClientOptions.setEnabled(mode != CoinJoinMode.NONE) if (mode != CoinJoinMode.NONE && this.mode == CoinJoinMode.NONE) { configureMixing() - updateBalance(walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.AVAILABLE)) } + updateBalance(walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.AVAILABLE)) val currentTimeSkew = getCurrentTimeSkew() updateState(mode, currentTimeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain) } @@ -389,7 +395,8 @@ class CoinJoinMixingService @Inject constructor( message: PoolMessage? ) { super.onSessionStarted(wallet, sessionId, denomination, message) - log.info("Session {} started. {}% mixed", sessionId, progress) + log.info("Session started: {}. {}% mixed. {} active sessions", sessionId, progress, activeSessions + 1) + updateActiveSessions() } override fun onSessionComplete( @@ -402,7 +409,8 @@ class CoinJoinMixingService @Inject constructor( joined: Boolean ) { super.onSessionComplete(wallet, sessionId, denomination, state, message, address, joined) - log.info("Session {} complete. {} % mixed -- {}", sessionId, progress, message) + log.info("Session completed: {}. {}% mixed. {} active sessions", sessionId, progress, activeSessions - 1) + updateActiveSessions() } override fun onTransactionProcessed(tx: Transaction?, type: CoinJoinTransactionType?, sessionId: Int) { @@ -470,9 +478,6 @@ class CoinJoinMixingService @Inject constructor( log.info("coinjoin: Mixing preparation began") clear() val wallet = walletDataProvider.wallet!! - addMixingCompleteListener(mixingProgressTracker) - addSessionCompleteListener(mixingProgressTracker) - addTransationListener(mixingProgressTracker) coinJoinManager?.run { clientManager = CoinJoinClientManager(wallet) coinJoinClientManagers[wallet.description] = clientManager @@ -483,6 +488,11 @@ class CoinJoinMixingService @Inject constructor( clientManager.setStopOnNothingToDo(true) val mixingFinished = clientManager.mixingFinishedFuture + addMixingCompleteListener(executor, mixingProgressTracker) + addSessionStartedListener(executor, mixingProgressTracker) + addSessionCompleteListener(executor, mixingProgressTracker) + addTransationListener(executor, mixingProgressTracker) + val mixingCompleteListener = MixingCompleteListener { _, statusList -> statusList?.let { @@ -522,8 +532,8 @@ class CoinJoinMixingService @Inject constructor( } }, Threading.USER_THREAD) - addMixingCompleteListener(Threading.USER_THREAD, mixingCompleteListener) - addSessionCompleteListener(Threading.USER_THREAD, sessionCompleteListener) + addMixingCompleteListener(executor, mixingCompleteListener) + addSessionCompleteListener(executor, sessionCompleteListener) log.info("coinjoin: mixing preparation finished") setRequestKeyParameter(requestKeyParameter) @@ -571,6 +581,7 @@ class CoinJoinMixingService @Inject constructor( // remove all listeners mixingCompleteListeners.forEach { coinJoinManager?.removeMixingCompleteListener(it) } sessionCompleteListeners.forEach { coinJoinManager?.removeSessionCompleteListener(it) } + sessionStartedListeners.forEach { coinJoinManager?.removeSessionStartedListener(it) } clientManager.stopMixing() coinJoinManager?.stop() context.unregisterReceiver(timeChangeReceiver) @@ -585,6 +596,10 @@ class CoinJoinMixingService @Inject constructor( this.blockChain = blockChain } + private fun addSessionStartedListener(sessionStartedListener: SessionStartedListener) { + sessionStartedListeners.add(sessionStartedListener) + coinJoinManager?.addSessionStartedListener(Threading.USER_THREAD, sessionStartedListener) + } private fun addSessionCompleteListener(sessionCompleteListener: SessionCompleteListener) { sessionCompleteListeners.add(sessionCompleteListener) coinJoinManager?.addSessionCompleteListener(Threading.USER_THREAD, sessionCompleteListener) @@ -604,20 +619,28 @@ class CoinJoinMixingService @Inject constructor( exception = null } - private suspend fun updateProgress() { + override suspend fun getMixingProgress(): Double { val wallet = walletDataProvider.wallet as WalletEx - val mixedBalance = wallet.coinJoinBalance - val anonymizableBalance = wallet.getAnonymizableBalance(false, false) - if (mixedBalance != Coin.ZERO && anonymizableBalance != Coin.ZERO) { - val progress = mixedBalance.value * 100.0 / (mixedBalance.value + anonymizableBalance.value) - log.info( - "coinjoin: progress {} = 100*{}/({} + {})", - progress, - mixedBalance.value, - mixedBalance.value, - anonymizableBalance.value - ) - _progressFlow.emit(progress) + return wallet.coinJoin.mixingProgress * 100.0 + } + + private suspend fun updateProgress() { + val progress = getMixingProgress() + _progressFlow.emit(progress) + } + + private fun updateActiveSessions() { + coroutineScope.launch { + activeSessions = if (this@CoinJoinMixingService::clientManager.isInitialized) { + clientManager.sessionsStatus?.count { poolStatus -> + poolStatus == PoolStatus.CONNECTING || poolStatus == PoolStatus.CONNECTED || poolStatus == PoolStatus.MIXING + } ?: 0 + } else { + 0 + } + log.info("coinjoin-activeSessions: {}", activeSessions) + activeSessionsFlow.emit(activeSessions) } } + override fun observeActiveSessions(): Flow = activeSessionsFlow } diff --git a/wallet/src/de/schildbach/wallet/service/ForegroundService.kt b/wallet/src/de/schildbach/wallet/service/ForegroundService.kt new file mode 100644 index 0000000000..83d27b7aeb --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/ForegroundService.kt @@ -0,0 +1,7 @@ +package de.schildbach.wallet.service + +enum class ForegroundService { + NONE, + BLOCKCHAIN_SYNC, + COINJOIN_MIXING +} \ No newline at end of file From cfa11c3aeb9fafca6112841087acc736bd764f47 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 29 Feb 2024 06:34:56 -0800 Subject: [PATCH 4/4] fix: improve timeskew functionality --- wallet/res/values/strings.xml | 1 + .../schildbach/wallet/service/CoinJoinService.kt | 2 +- .../de/schildbach/wallet/ui/SettingsActivity.kt | 10 ++-------- .../de/schildbach/wallet/ui/main/MainActivity.kt | 14 ++++++++++++++ .../de/schildbach/wallet/ui/main/MainViewModel.kt | 6 +++--- .../schildbach/wallet/ui/main/WalletActivityExt.kt | 6 +++--- wallet/src/de/schildbach/wallet/util/TimeUtils.kt | 11 ++++++++++- 7 files changed, 34 insertions(+), 16 deletions(-) diff --git a/wallet/res/values/strings.xml b/wallet/res/values/strings.xml index 6fa98e3d9e..0fc083b268 100644 --- a/wallet/res/values/strings.xml +++ b/wallet/res/values/strings.xml @@ -518,6 +518,7 @@ Stop Mixing Mixing Mixing Paused + Not Started %s (%d%%) %s of %s %1$s of %2$s Fully Mixed diff --git a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt index 30e1b56796..34ed887c8d 100644 --- a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt +++ b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt @@ -176,7 +176,7 @@ class CoinJoinMixingService @Inject constructor( // Time has changed, handle the change here log.info("Time or Time Zone changed") coroutineScope.launch { - updateTimeSkewInternal(getTimeSkew()) + updateTimeSkewInternal(getTimeSkew(force = true)) } } } diff --git a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt index 13999677c1..9ef3421661 100644 --- a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt @@ -170,6 +170,7 @@ class SettingsActivity : LockScreenActivity() { binding.progressBar.isVisible = false } else { @StringRes val statusId = when(viewModel.coinJoinMixingStatus) { + MixingStatus.NOT_STARTED -> R.string.coinjoin_not_started MixingStatus.MIXING -> R.string.coinjoin_mixing MixingStatus.PAUSED -> R.string.coinjoin_paused MixingStatus.FINISHED -> R.string.coinjoin_progress_finished @@ -177,15 +178,8 @@ class SettingsActivity : LockScreenActivity() { } binding.coinjoinSubtitle.text = getString(statusId) -// getString( -// R.string.coinjoin_progress, -// getString(statusId), -// viewModel.mixingProgress.toInt(), -// viewModel.mixedBalance, -// viewModel.walletBalance -// ) binding.coinjoinSubtitleIcon.isVisible = true - binding.progressBar.isVisible = true + binding.progressBar.isVisible = viewModel.coinJoinMixingStatus == MixingStatus.MIXING binding.coinjoinProgress.text = getString(R.string.percent, viewModel.mixingProgress.toInt()) binding.balance.isVisible = true binding.balance.text = getString(R.string.coinjoin_progress_balance, viewModel.mixedBalance, viewModel.walletBalance) diff --git a/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt b/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt index 50291f725a..4506cbb1ed 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt @@ -113,6 +113,18 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm requestDisableBatteryOptimisation() } + private val timeChangeReceiver = object: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_TIME_CHANGED) { + // Time has changed, handle the change here + log.info("Time or Time Zone changed") + lifecycleScope.launch { + checkTimeSkew(viewModel, force = true) + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) @@ -148,6 +160,7 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm val timeChangedFilter = IntentFilter().apply { addAction(Intent.ACTION_TIME_CHANGED) } + registerReceiver(timeChangeReceiver, timeChangedFilter) } override fun onStart() { @@ -561,6 +574,7 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm override fun onDestroy() { super.onDestroy() viewModel.platformRepo.onIdentityResolved = null + unregisterReceiver(timeChangeReceiver) } override fun onLockScreenDeactivated() { diff --git a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt index b60cd56ef5..c3ec9faffd 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt @@ -388,13 +388,13 @@ class MainViewModel @Inject constructor( return coinJoinConfig.getMode() } - suspend fun getDeviceTimeSkew(): Pair { + suspend fun getDeviceTimeSkew(force: Boolean): Pair { return try { - val timeSkew = getTimeSkew() + val timeSkew = getTimeSkew(force) val maxAllowedTimeSkew: Long = if (coinJoinConfig.getMode() == CoinJoinMode.NONE) { TIME_SKEW_TOLERANCE } else { - if (timeSkew > 0) MAX_ALLOWED_AHEAD_TIMESKEW else MAX_ALLOWED_BEHIND_TIMESKEW + if (timeSkew > 0) MAX_ALLOWED_AHEAD_TIMESKEW * 3 else MAX_ALLOWED_BEHIND_TIMESKEW * 2 } coinJoinService.updateTimeSkew(timeSkew) log.info("timeskew: {} ms", timeSkew) diff --git a/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt b/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt index 28e1a68115..c07d683ade 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt @@ -95,11 +95,11 @@ object WalletActivityExt { } } - fun MainActivity.checkTimeSkew(viewModel: MainViewModel) { + fun MainActivity.checkTimeSkew(viewModel: MainViewModel, force: Boolean = false) { lifecycleScope.launch { - val (isTimeSkewed, timeSkew) = viewModel.getDeviceTimeSkew() + val (isTimeSkewed, timeSkew) = viewModel.getDeviceTimeSkew(force) val coinJoinOn = viewModel.getCoinJoinMode() != CoinJoinMode.NONE - if (isTimeSkewed && (!timeSkewDialogShown || coinJoinOn)) { + if (isTimeSkewed && (!timeSkewDialogShown || force)) { timeSkewDialogShown = true // add 1 to round up so 2.2 seconds appears as 3 showTimeSkewAlertDialog((if (timeSkew > 0) 1 else -1) + timeSkew / 1000L, coinJoinOn) diff --git a/wallet/src/de/schildbach/wallet/util/TimeUtils.kt b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt index de39a15b09..1e9cf4422c 100644 --- a/wallet/src/de/schildbach/wallet/util/TimeUtils.kt +++ b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt @@ -44,8 +44,15 @@ private fun queryNtpTime(server: String): Long? { return null } +// check no more than once per minute +private var lastTimeWhenSkewChecked = 0L +private var lastTimeSkew = 0L @Throws(NullPointerException::class) -suspend fun getTimeSkew(): Long { +suspend fun getTimeSkew(force: Boolean = false): Long { + if (!force && (lastTimeWhenSkewChecked + 60 * 1000 > System.currentTimeMillis())) { + log.info("timeskew: {}; using last value", lastTimeSkew) + return lastTimeSkew + } var networkTime: Long? = null var timeSource = "NTP" @@ -87,6 +94,8 @@ suspend fun getTimeSkew(): Long { } val systemTimeMillis = System.currentTimeMillis() + lastTimeWhenSkewChecked = systemTimeMillis + lastTimeSkew = systemTimeMillis - networkTime log.info("timeskew: $systemTimeMillis-$networkTime = ${systemTimeMillis - networkTime}; source: $timeSource") return systemTimeMillis - networkTime }