Convert legacy Long millis overloads to kotlin.time.Duration

Replace delay/withTimeoutOrNull/debounce Long-millis calls with their
kotlin.time.Duration overloads, convert millis timeout constants to
Duration vals, and switch retryWithBackoff, the relay reconnect backoff
and the sensitive-clipboard clear delay to Duration parameters.
This commit is contained in:
greenart7c3
2026-08-19 14:04:19 -03:00
parent abcf2f7703
commit a49caa0881
16 changed files with 79 additions and 60 deletions
@@ -76,6 +76,7 @@ import java.net.Socket
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -410,11 +411,11 @@ class Amber :
if (attempt > 0) {
TorManager.showRetrying()
TorManager.stop()
delay(3000)
delay(3.seconds)
TorManager.start(this@Amber, applicationIOScope)
}
attempt++
withTimeoutOrNull(120_000L) {
withTimeoutOrNull(120.seconds) {
TorManager.isRunning.first { it }
}
}
@@ -553,7 +554,7 @@ class Amber :
}
AmberLog.d(TAG, "checkForNewRelaysAndUpdateAllFilters wasActive: $wasActive")
if (!wasActive) {
delay(3000)
delay(3.seconds)
client.connect()
}
@@ -15,6 +15,7 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -142,7 +143,7 @@ class MainViewModel(val context: Context) : ViewModel() {
var error = true
var count = 0
while (error && count < 10) {
delay(100)
delay(100.milliseconds)
count++
try {
if (route == Route.UpdateSettings.route) {
@@ -24,6 +24,7 @@ import com.greenart7c3.nostrsigner.service.StopServiceReceiver
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -62,7 +63,7 @@ class AmberRelayStats(
@SuppressLint("MissingPermission")
val relayStatus = combine(client.availableRelaysFlow(), client.connectedRelaysFlow()) { available, connected ->
available to connected
}.debounce(300).onEach {
}.debounce(300.milliseconds).onEach {
this.available = it.first
this.connected = it.second
val notificationManager = NotificationManagerCompat.from(appContext)
@@ -115,13 +116,13 @@ class AmberRelayStats(
notificationManager.createNotificationChannel(statusChannel)
Amber.instance.applicationIOScope.launch {
Amber.instance.client.availableRelaysFlow().debounce(300).collect {
Amber.instance.client.availableRelaysFlow().debounce(300.milliseconds).collect {
available = it
updateNotification()
}
}
Amber.instance.applicationIOScope.launch {
Amber.instance.client.connectedRelaysFlow().debounce(300).collect {
Amber.instance.client.connectedRelaysFlow().debounce(300.milliseconds).collect {
connected = it
updateNotification()
}
@@ -130,7 +131,7 @@ class AmberRelayStats(
// Trailing-edge debounce for the counter path (addSent/addFailed):
// the last tick of a burst always renders, at most one notify per
// 300ms window.
counterTick.debounce(300).collect {
counterTick.debounce(300.milliseconds).collect {
updateNotification()
}
}
@@ -17,6 +17,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -52,7 +53,7 @@ class NostrClientLoggerListener(
val scope: CoroutineScope,
) : RelayConnectionListener {
private var reconnectJob: Job? = null
private var reconnectDelay = 5_000L
private var reconnectDelay = 5.seconds
private var lastDisconnectTime = 0L
// Counts the failure against the relay and only schedules a reconnect while it
@@ -71,15 +72,15 @@ class NostrClientLoggerListener(
private fun reconnectWithBackoff() {
val now = System.currentTimeMillis()
if (now - lastDisconnectTime > 60_000) {
reconnectDelay = 5_000L
reconnectDelay = 5.seconds
}
lastDisconnectTime = now
reconnectJob?.cancel()
reconnectJob = scope.launch {
AmberLog.d(Amber.TAG, "Reconnecting in ${reconnectDelay / 1000}s...")
AmberLog.d(Amber.TAG, "Reconnecting in ${reconnectDelay.inWholeSeconds}s...")
delay(reconnectDelay)
reconnectDelay = (reconnectDelay * 2).coerceAtMost(60_000L)
reconnectDelay = (reconnectDelay * 2).coerceAtMost(60.seconds)
if (!BuildFlavorChecker.isOfflineFlavor() && !Amber.instance.settings.killSwitch.value) {
Amber.instance.reconnect()
}
@@ -24,13 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
private const val BACKUP_KIND = 30078
private const val BACKUP_D_TAG = "amber-app-backup"
private const val INBOX_KIND = 10002
private const val INBOX_FETCH_TIMEOUT_MS = 10_000L
private const val BACKUP_FETCH_TIMEOUT_MS = 15_000L
private val INBOX_FETCH_TIMEOUT = 10.seconds
private val BACKUP_FETCH_TIMEOUT = 15.seconds
private const val PAYLOAD_VERSION = 1
private val AGGREGATOR_RELAY = RelayUrlNormalizer.normalizeOrNull("wss://aggr.nostr.land/")
@@ -181,7 +182,7 @@ object ApplicationBackup {
limit = 1,
)
client.subscribe(subId, discoveryRelays.associateWith { listOf(filter) })
delay(INBOX_FETCH_TIMEOUT_MS)
delay(INBOX_FETCH_TIMEOUT)
} catch (e: Exception) {
if (e is CancellationException) throw e
AmberLog.w(Amber.TAG, "ApplicationBackup: inbox relay fetch failed", e)
@@ -273,7 +274,7 @@ object ApplicationBackup {
limit = 1,
)
client.subscribe(subId, relays.associateWith { listOf(filter) })
delay(BACKUP_FETCH_TIMEOUT_MS)
delay(BACKUP_FETCH_TIMEOUT)
} catch (e: Exception) {
if (e is CancellationException) throw e
AmberLog.e(Amber.TAG, "ApplicationBackup: failed to fetch backup event", e)
@@ -37,6 +37,8 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.toSet
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@@ -218,11 +220,11 @@ object BunkerRequestUtils {
suspend fun retryWithBackoff(
maxRetries: Int = 5,
initialDelayMs: Long = 200L,
maxDelayMs: Long = 3_200L,
initialDelay: Duration = 200.milliseconds,
maxDelay: Duration = 3_200.milliseconds,
block: suspend () -> Boolean,
): Boolean {
var currentDelay = initialDelayMs
var currentDelay = initialDelay
repeat(maxRetries) { attempt ->
delay(currentDelay)
@@ -231,7 +233,7 @@ object BunkerRequestUtils {
}
if (attempt < maxRetries - 1) {
currentDelay = (currentDelay * 2).coerceAtMost(maxDelayMs)
currentDelay = (currentDelay * 2).coerceAtMost(maxDelay)
}
}
@@ -397,7 +399,7 @@ object BunkerRequestUtils {
activity?.finishAndRemoveTask()
}
delay(500)
delay(500.milliseconds)
if (signPolicy != null) {
AmberUtils.configureSignPolicy(application, signPolicy, key, permissions)
@@ -40,13 +40,14 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
private const val EOSE_TIMEOUT_MS = 30_000L
private val EOSE_TIMEOUT = 30.seconds
class ProfileSubscription(
val client: NostrClient,
@@ -170,7 +171,7 @@ class ProfileSubscription(
relaysPerSubId[subId] = ConcurrentHashMap.newKeySet<NormalizedRelayUrl>().apply { addAll(relayListFilter.keys) }
client.subscribe(subId, relayListFilter)
timeoutJobs[subId] = scope.launch {
delay(EOSE_TIMEOUT_MS)
delay(EOSE_TIMEOUT)
if (relaysPerSubId.containsKey(subId)) {
unsubscribe(subId)
// still fetch the profile with whatever relay list we have saved
@@ -186,7 +187,7 @@ class ProfileSubscription(
relaysPerSubId[subId] = ConcurrentHashMap.newKeySet<NormalizedRelayUrl>().apply { addAll(profileFilter.keys) }
client.subscribe(subId, profileFilter)
timeoutJobs[subId] = scope.launch {
delay(EOSE_TIMEOUT_MS)
delay(EOSE_TIMEOUT)
if (relaysPerSubId.containsKey(subId)) {
unsubscribe(subId)
}
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -109,7 +110,7 @@ object ReportSender {
)
}
onDone()
delay(10000)
delay(10.seconds)
client.disconnect()
}
}
@@ -8,10 +8,11 @@ import com.greenart7c3.nostrsigner.AmberLog
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
private const val CHECK_TIMEOUT_MS = 30_000L
private val CHECK_TIMEOUT = 30.seconds
class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
@@ -29,7 +30,7 @@ class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : C
// If a check was actually started, wait for it to finish so the notification fires
// before WorkManager considers the job done.
if (updater.isChecking.value) {
withTimeoutOrNull(CHECK_TIMEOUT_MS) {
withTimeoutOrNull(CHECK_TIMEOUT) {
updater.isChecking.first { !it }
}
}
@@ -28,6 +28,7 @@ import java.io.File
import java.security.MessageDigest
import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -38,7 +39,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Request
private const val EOSE_TIMEOUT_MS = 15_000L
private val EOSE_TIMEOUT = 15.seconds
private const val RELEASE_KIND = 30063
private val UPDATE_RELAY_URLS = listOf(
"wss://relay.zapstore.dev",
@@ -129,7 +130,7 @@ class ZapstoreUpdater(
}
timeoutJob = scope.launch {
delay(EOSE_TIMEOUT_MS)
delay(EOSE_TIMEOUT)
AmberLog.w(Amber.TAG, "ZapstoreUpdater: timeout waiting for EOSE")
onReleaseEose()
}
@@ -339,7 +340,7 @@ class ZapstoreUpdater(
}
context.startActivity(intent)
scope.launch(Dispatchers.Main) {
delay(2000)
delay(2.seconds)
downloadState.value = DownloadState.IDLE
}
}
@@ -119,6 +119,7 @@ import com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.RandomInstance
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -1055,7 +1056,7 @@ fun LoginPage(
if (isValid.first != null) {
keyPair = isValid.first!!
scope.launch {
delay(200)
delay(200.milliseconds)
pageState.animateScrollToPage(1)
}
} else {
@@ -1139,7 +1140,7 @@ fun LoginPage(
if (isValid.first != null) {
keyPair = isValid.first!!
scope.launch {
delay(200)
delay(200.milliseconds)
pageState.animateScrollToPage(1)
}
} else {
@@ -1182,7 +1183,7 @@ fun LoginPage(
keyPair = isValid.first!!
keyboardController?.hide()
scope.launch {
delay(200)
delay(200.milliseconds)
pageState.animateScrollToPage(1)
}
} else {
@@ -1212,7 +1213,7 @@ fun LoginPage(
keyPair = isValid.first!!
keyboardController?.hide()
scope.launch {
delay(200)
delay(200.milliseconds)
pageState.animateScrollToPage(1)
}
} else {
@@ -7,12 +7,14 @@ import android.os.PersistableBundle
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import com.greenart7c3.nostrsigner.Amber
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** How long a copied secret stays on the clipboard before it is cleared. */
const val SENSITIVE_CLIPBOARD_CLEAR_DELAY_MS = 60_000L
val SENSITIVE_CLIPBOARD_CLEAR_DELAY = 60.seconds
/**
* Creates a [ClipData] flagged as sensitive so the system (Android 13+) avoids
@@ -34,7 +36,7 @@ fun newSensitivePlainText(label: CharSequence, text: CharSequence): ClipData {
/**
* Copies a secret to the clipboard flagged as sensitive content and schedules it
* to be cleared after [clearAfterMillis]. The clipboard is only cleared if it
* to be cleared after [clearAfter]. The clipboard is only cleared if it
* still contains the copied secret, so anything the user copies afterwards is
* left untouched.
*/
@@ -42,19 +44,19 @@ suspend fun Clipboard.setSensitiveClip(
label: CharSequence,
text: CharSequence,
scope: CoroutineScope = Amber.instance.applicationIOScope,
clearAfterMillis: Long = SENSITIVE_CLIPBOARD_CLEAR_DELAY_MS,
clearAfter: Duration = SENSITIVE_CLIPBOARD_CLEAR_DELAY,
) {
setClipEntry(ClipEntry(newSensitivePlainText(label, text)))
scheduleSensitiveClear(text, scope, clearAfterMillis)
scheduleSensitiveClear(text, scope, clearAfter)
}
private fun Clipboard.scheduleSensitiveClear(
copiedValue: CharSequence,
scope: CoroutineScope,
delayMillis: Long,
clearAfter: Duration,
) {
scope.launch {
delay(delayMillis)
delay(clearAfter)
val currentText = getClipEntry()?.clipData?.let { clip ->
if (clip.itemCount > 0) clip.getItemAt(0).text?.toString() else null
}
@@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -177,7 +178,7 @@ fun TranslationReportScreen(
Amber.instance.pendingTranslationReport.value = null
onLoading(false)
onDismiss()
delay(10000)
delay(10.seconds)
client.disconnect()
}
}
@@ -89,6 +89,8 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.Base64
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -158,7 +160,7 @@ fun DefaultRelaysScreen(
scope.launch(Dispatchers.IO) {
if (!BuildFlavorChecker.isOfflineFlavor()) {
Amber.instance.checkForNewRelaysAndUpdateAllFilters()
delay(2000)
delay(2.seconds)
Amber.instance.client.reconnect()
isLoading.value = false
} else {
@@ -458,9 +460,9 @@ fun onAddRelay(
mapOf(addedWSS to filters),
)
val canContinue = withTimeoutOrNull(30000) {
val canContinue = withTimeoutOrNull(30.seconds) {
while (!canSendRequest) {
delay(200)
delay(200.milliseconds)
}
true
}
@@ -489,7 +491,7 @@ fun onAddRelay(
success = client.publishAndConfirm(signedEvent, setOf(addedWSS))
if (!success) {
errorCount++
delay(1000)
delay(1.seconds)
signedEvent = signer.signerSync.sign(
TimeUtils.now(),
NostrConnectEvent.KIND,
@@ -503,7 +505,7 @@ fun onAddRelay(
AmberListenerSingleton.latestErrorMessages.clear()
var count = 0
while (!filterResult && count < 10) {
delay(1000)
delay(1.seconds)
count++
}
} else {
@@ -3,6 +3,7 @@ package com.greenart7c3.nostrsigner
import java.net.URI
import kotlin.system.measureNanoTime
import kotlin.system.measureTimeMillis
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
@@ -78,7 +79,7 @@ class SignerProviderBenchmarkTest {
runBlocking {
val warmup = MutableStateFlow(true)
launch {
delay(1)
delay(1.milliseconds)
warmup.value = false
}
warmup.first { !it }
@@ -88,7 +89,7 @@ class SignerProviderBenchmarkTest {
val state = MutableStateFlow(true)
runBlocking {
launch {
delay(releaseAfterMs)
delay(releaseAfterMs.milliseconds)
state.value = false
}
state.first { !it }
@@ -6,6 +6,7 @@ import com.greenart7c3.nostrsigner.models.SignerType
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import io.mockk.every
import io.mockk.mockk
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
@@ -344,8 +345,8 @@ class BunkerRequestUtilsTest {
var calls = 0
val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 3,
initialDelayMs = 1L,
maxDelayMs = 4L,
initialDelay = 1.milliseconds,
maxDelay = 4.milliseconds,
) {
calls++
true
@@ -359,8 +360,8 @@ class BunkerRequestUtilsTest {
var calls = 0
val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 5,
initialDelayMs = 1L,
maxDelayMs = 4L,
initialDelay = 1.milliseconds,
maxDelay = 4.milliseconds,
) {
calls++
calls >= 3
@@ -374,8 +375,8 @@ class BunkerRequestUtilsTest {
var calls = 0
val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 3,
initialDelayMs = 1L,
maxDelayMs = 4L,
initialDelay = 1.milliseconds,
maxDelay = 4.milliseconds,
) {
calls++
false
@@ -389,8 +390,8 @@ class BunkerRequestUtilsTest {
var calls = 0
val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 1,
initialDelayMs = 1L,
maxDelayMs = 4L,
initialDelay = 1.milliseconds,
maxDelay = 4.milliseconds,
) {
calls++
false
@@ -403,8 +404,8 @@ class BunkerRequestUtilsTest {
fun `retryWithBackoff with maxRetries 1 returns true on success`() = runBlocking {
val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 1,
initialDelayMs = 1L,
maxDelayMs = 4L,
initialDelay = 1.milliseconds,
maxDelay = 4.milliseconds,
) { true }
assertTrue(result)
}
@@ -415,8 +416,8 @@ class BunkerRequestUtilsTest {
var calls = 0
BunkerRequestUtils.retryWithBackoff(
maxRetries = maxRetries,
initialDelayMs = 1L,
maxDelayMs = 4L,
initialDelay = 1.milliseconds,
maxDelay = 4.milliseconds,
) {
calls++
false