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/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/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
+ Not Started
+ %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/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..34ed887c8d 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)
@@ -171,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))
}
}
}
@@ -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
diff --git a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt
index 4fc3ef36b6..9ef3421661 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,29 @@ 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.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
+ else -> R.string.error
+ }
+
+ binding.coinjoinSubtitle.text = getString(statusId)
binding.coinjoinSubtitleIcon.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 5ea27f5ae8..c3ec9faffd 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
@@ -386,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/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
diff --git a/wallet/src/de/schildbach/wallet/util/TimeUtils.kt b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt
index f3238fbe82..1e9cf4422c 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,39 @@ private fun queryNtpTime(server: String): Long? {
return null
}
-suspend fun getTimeSkew(): Long {
+// check no more than once per minute
+private var lastTimeWhenSkewChecked = 0L
+private var lastTimeSkew = 0L
+@Throws(NullPointerException::class)
+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
- 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 +84,18 @@ 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()
+ lastTimeWhenSkewChecked = systemTimeMillis
+ lastTimeSkew = systemTimeMillis - networkTime
+ log.info("timeskew: $systemTimeMillis-$networkTime = ${systemTimeMillis - networkTime}; source: $timeSource")
return systemTimeMillis - networkTime
}