Convert legacy Long timing overloads to Duration, silence flavor warnings

Replace every Long-millisecond call to delay/withTimeoutOrNull/debounce
with the kotlin.time.Duration overloads, converting the related
constants (EOSE/backup/check timeouts, clipboard clear delay) and the
retryWithBackoff/reconnect backoff state to Duration as well.

Also suppress the three deliberate cross-flavor compiler warnings in
TrustScoreService, WebDavService and ZapstoreUpdater: the offline
flavor compiles against OkHttp 4 where response.body is nullable, so
the safe call/elvis is required there and only redundant in the free
flavor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho6LW6eEBkVnnpFvr5tvpi
This commit is contained in:
Claude
2026-07-08 21:05:34 +00:00
parent fb984b8db4
commit 9b65375138
19 changed files with 85 additions and 61 deletions
@@ -75,6 +75,8 @@ import java.net.Socket
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -402,11 +404,11 @@ class Amber :
if (attempt > 0) { if (attempt > 0) {
TorManager.showRetrying() TorManager.showRetrying()
TorManager.stop() TorManager.stop()
delay(3000) delay(3.seconds)
TorManager.start(this@Amber, applicationIOScope) TorManager.start(this@Amber, applicationIOScope)
} }
attempt++ attempt++
withTimeoutOrNull(120_000L) { withTimeoutOrNull(2.minutes) {
TorManager.isRunning.first { it } TorManager.isRunning.first { it }
} }
} }
@@ -545,7 +547,7 @@ class Amber :
} }
AmberLog.d(TAG, "checkForNewRelaysAndUpdateAllFilters wasActive: $wasActive") AmberLog.d(TAG, "checkForNewRelaysAndUpdateAllFilters wasActive: $wasActive")
if (!wasActive) { if (!wasActive) {
delay(3000) delay(3.seconds)
client.connect() client.connect()
} }
@@ -15,6 +15,7 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -142,7 +143,7 @@ class MainViewModel(val context: Context) : ViewModel() {
var error = true var error = true
var count = 0 var count = 0
while (error && count < 10) { while (error && count < 10) {
delay(100) delay(100.milliseconds)
count++ count++
try { try {
if (route == Route.UpdateSettings.route) { 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.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@@ -43,7 +44,7 @@ class AmberRelayStats(
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
val relayStatus = combine(client.availableRelaysFlow(), client.connectedRelaysFlow()) { available, connected -> val relayStatus = combine(client.availableRelaysFlow(), client.connectedRelaysFlow()) { available, connected ->
available to connected available to connected
}.debounce(300).onEach { }.debounce(300.milliseconds).onEach {
this.available = it.first this.available = it.first
this.connected = it.second this.connected = it.second
val notificationManager = NotificationManagerCompat.from(appContext) val notificationManager = NotificationManagerCompat.from(appContext)
@@ -96,13 +97,13 @@ class AmberRelayStats(
notificationManager.createNotificationChannel(statusChannel) notificationManager.createNotificationChannel(statusChannel)
Amber.instance.applicationIOScope.launch { Amber.instance.applicationIOScope.launch {
Amber.instance.client.availableRelaysFlow().debounce(300).collect { Amber.instance.client.availableRelaysFlow().debounce(300.milliseconds).collect {
available = it available = it
updateNotification() updateNotification()
} }
} }
Amber.instance.applicationIOScope.launch { Amber.instance.applicationIOScope.launch {
Amber.instance.client.connectedRelaysFlow().debounce(300).collect { Amber.instance.client.connectedRelaysFlow().debounce(300.milliseconds).collect {
connected = it connected = it
updateNotification() 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.EventCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -52,7 +53,7 @@ class NostrClientLoggerListener(
val scope: CoroutineScope, val scope: CoroutineScope,
) : RelayConnectionListener { ) : RelayConnectionListener {
private var reconnectJob: Job? = null private var reconnectJob: Job? = null
private var reconnectDelay = 5_000L private var reconnectDelay = 5.seconds
private var lastDisconnectTime = 0L private var lastDisconnectTime = 0L
// Counts the failure against the relay and only schedules a reconnect while it // Counts the failure against the relay and only schedules a reconnect while it
@@ -71,15 +72,15 @@ class NostrClientLoggerListener(
private fun reconnectWithBackoff() { private fun reconnectWithBackoff() {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastDisconnectTime > 60_000) { if (now - lastDisconnectTime > 60_000) {
reconnectDelay = 5_000L reconnectDelay = 5.seconds
} }
lastDisconnectTime = now lastDisconnectTime = now
reconnectJob?.cancel() reconnectJob?.cancel()
reconnectJob = scope.launch { reconnectJob = scope.launch {
AmberLog.d(Amber.TAG, "Reconnecting in ${reconnectDelay / 1000}s...") AmberLog.d(Amber.TAG, "Reconnecting in ${reconnectDelay.inWholeSeconds}s...")
delay(reconnectDelay) delay(reconnectDelay)
reconnectDelay = (reconnectDelay * 2).coerceAtMost(60_000L) reconnectDelay = (reconnectDelay * 2).coerceAtMost(60.seconds)
if (!BuildFlavorChecker.isOfflineFlavor() && !Amber.instance.settings.killSwitch.value) { if (!BuildFlavorChecker.isOfflineFlavor() && !Amber.instance.settings.killSwitch.value) {
Amber.instance.reconnect() Amber.instance.reconnect()
} }
@@ -24,13 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
private const val BACKUP_KIND = 30078 private const val BACKUP_KIND = 30078
private const val BACKUP_D_TAG = "amber-app-backup" private const val BACKUP_D_TAG = "amber-app-backup"
private const val INBOX_KIND = 10002 private const val INBOX_KIND = 10002
private const val INBOX_FETCH_TIMEOUT_MS = 10_000L private val INBOX_FETCH_TIMEOUT = 10.seconds
private const val BACKUP_FETCH_TIMEOUT_MS = 15_000L private val BACKUP_FETCH_TIMEOUT = 15.seconds
private const val PAYLOAD_VERSION = 1 private const val PAYLOAD_VERSION = 1
private val AGGREGATOR_RELAY = RelayUrlNormalizer.normalizeOrNull("wss://aggr.nostr.land/") private val AGGREGATOR_RELAY = RelayUrlNormalizer.normalizeOrNull("wss://aggr.nostr.land/")
@@ -182,7 +183,7 @@ object ApplicationBackup {
limit = 1, limit = 1,
) )
client.subscribe(subId, discoveryRelays.associateWith { listOf(filter) }) client.subscribe(subId, discoveryRelays.associateWith { listOf(filter) })
delay(INBOX_FETCH_TIMEOUT_MS) delay(INBOX_FETCH_TIMEOUT)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
AmberLog.w(Amber.TAG, "ApplicationBackup: inbox relay fetch failed", e) AmberLog.w(Amber.TAG, "ApplicationBackup: inbox relay fetch failed", e)
@@ -274,7 +275,7 @@ object ApplicationBackup {
limit = 1, limit = 1,
) )
client.subscribe(subId, relays.associateWith { listOf(filter) }) client.subscribe(subId, relays.associateWith { listOf(filter) })
delay(BACKUP_FETCH_TIMEOUT_MS) delay(BACKUP_FETCH_TIMEOUT)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
AmberLog.e(Amber.TAG, "ApplicationBackup: failed to fetch backup event", e) AmberLog.e(Amber.TAG, "ApplicationBackup: failed to fetch backup event", e)
@@ -36,6 +36,8 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.toSet import kotlin.collections.toSet
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@@ -213,11 +215,11 @@ object BunkerRequestUtils {
suspend fun retryWithBackoff( suspend fun retryWithBackoff(
maxRetries: Int = 5, maxRetries: Int = 5,
initialDelayMs: Long = 200L, initialDelay: Duration = 200.milliseconds,
maxDelayMs: Long = 3_200L, maxDelay: Duration = 3_200.milliseconds,
block: suspend () -> Boolean, block: suspend () -> Boolean,
): Boolean { ): Boolean {
var currentDelay = initialDelayMs var currentDelay = initialDelay
repeat(maxRetries) { attempt -> repeat(maxRetries) { attempt ->
delay(currentDelay) delay(currentDelay)
@@ -226,7 +228,7 @@ object BunkerRequestUtils {
} }
if (attempt < maxRetries - 1) { if (attempt < maxRetries - 1) {
currentDelay = (currentDelay * 2).coerceAtMost(maxDelayMs) currentDelay = (currentDelay * 2).coerceAtMost(maxDelay)
} }
} }
@@ -392,7 +394,7 @@ object BunkerRequestUtils {
activity?.finishAndRemoveTask() activity?.finishAndRemoveTask()
} }
delay(500) delay(500.milliseconds)
if (signPolicy != null) { if (signPolicy != null) {
AmberUtils.configureSignPolicy(application, signPolicy, key, permissions) AmberUtils.configureSignPolicy(application, signPolicy, key, permissions)
@@ -39,13 +39,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID import java.util.UUID
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val EOSE_TIMEOUT_MS = 30_000L private val EOSE_TIMEOUT = 30.seconds
class ProfileSubscription( class ProfileSubscription(
val client: NostrClient, val client: NostrClient,
@@ -165,7 +166,7 @@ class ProfileSubscription(
relaysPerSubId[subId] = relayListFilter.keys.toMutableSet() relaysPerSubId[subId] = relayListFilter.keys.toMutableSet()
client.subscribe(subId, relayListFilter) client.subscribe(subId, relayListFilter)
timeoutJobs[subId] = scope.launch { timeoutJobs[subId] = scope.launch {
delay(EOSE_TIMEOUT_MS) delay(EOSE_TIMEOUT)
if (relaysPerSubId.containsKey(subId)) { if (relaysPerSubId.containsKey(subId)) {
unsubscribe(subId) unsubscribe(subId)
// still fetch the profile with whatever relay list we have saved // still fetch the profile with whatever relay list we have saved
@@ -181,7 +182,7 @@ class ProfileSubscription(
relaysPerSubId[subId] = profileFilter.keys.toMutableSet() relaysPerSubId[subId] = profileFilter.keys.toMutableSet()
client.subscribe(subId, profileFilter) client.subscribe(subId, profileFilter)
timeoutJobs[subId] = scope.launch { timeoutJobs[subId] = scope.launch {
delay(EOSE_TIMEOUT_MS) delay(EOSE_TIMEOUT)
if (relaysPerSubId.containsKey(subId)) { if (relaysPerSubId.containsKey(subId)) {
unsubscribe(subId) unsubscribe(subId)
} }
@@ -3,6 +3,7 @@ package com.greenart7c3.nostrsigner.service
import android.content.Context import android.content.Context
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -17,7 +18,7 @@ class RelayDisconnectService(ctx: Context, params: WorkerParameters) : Worker(ct
val job = val job =
ioScope.launch { ioScope.launch {
val url = inputData.getString("relay") val url = inputData.getString("relay")
delay(60000) delay(1.minutes)
url?.let { url?.let {
// TODO: this class is not being used, but this new way of using Nostr client requires removing the relay from all filters to disconnect it // TODO: this class is not being used, but this new way of using Nostr client requires removing the relay from all filters to disconnect it
// val relay = Amber.instance.client.getRelay(it) // val relay = Amber.instance.client.getRelay(it)
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -109,7 +110,7 @@ object ReportSender {
) )
} }
onDone() onDone()
delay(10000) delay(10.seconds)
client.disconnect() client.disconnect()
} }
} }
@@ -108,6 +108,7 @@ object TrustScoreService {
} }
// Safe call needed: the offline flavor compiles against OkHttp 4, where body is nullable // Safe call needed: the offline flavor compiles against OkHttp 4, where body is nullable
@Suppress("UNNECESSARY_SAFE_CALL")
val body = response.body?.string() val body = response.body?.string()
if (body == null) { if (body == null) {
cacheResult(normalizedUrl, null, isFailed = true) cacheResult(normalizedUrl, null, isFailed = true)
@@ -8,10 +8,11 @@ import com.greenart7c3.nostrsigner.AmberLog
import com.greenart7c3.nostrsigner.BuildConfig import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.BuildFlavorChecker import com.greenart7c3.nostrsigner.BuildFlavorChecker
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull 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) { 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 // If a check was actually started, wait for it to finish so the notification fires
// before WorkManager considers the job done. // before WorkManager considers the job done.
if (updater.isChecking.value) { if (updater.isChecking.value) {
withTimeoutOrNull(CHECK_TIMEOUT_MS) { withTimeoutOrNull(CHECK_TIMEOUT) {
updater.isChecking.first { !it } updater.isChecking.first { !it }
} }
} }
@@ -67,6 +67,7 @@ object WebDavService {
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
// Safe call needed: the offline flavor compiles against OkHttp 4, where body is nullable // Safe call needed: the offline flavor compiles against OkHttp 4, where body is nullable
@Suppress("UNNECESSARY_SAFE_CALL", "USELESS_ELVIS")
val body = response.body?.string() ?: "" val body = response.body?.string() ?: ""
Result.success(body) Result.success(body)
} else { } else {
@@ -26,6 +26,7 @@ import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import java.util.UUID import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -36,7 +37,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
private const val EOSE_TIMEOUT_MS = 15_000L private val EOSE_TIMEOUT = 15.seconds
private const val RELEASE_KIND = 30063 private const val RELEASE_KIND = 30063
private val UPDATE_RELAY_URLS = listOf( private val UPDATE_RELAY_URLS = listOf(
"wss://relay.zapstore.dev", "wss://relay.zapstore.dev",
@@ -128,7 +129,7 @@ class ZapstoreUpdater(
} }
timeoutJob = scope.launch { timeoutJob = scope.launch {
delay(EOSE_TIMEOUT_MS) delay(EOSE_TIMEOUT)
AmberLog.w(Amber.TAG, "ZapstoreUpdater: timeout waiting for EOSE") AmberLog.w(Amber.TAG, "ZapstoreUpdater: timeout waiting for EOSE")
onReleaseEose() onReleaseEose()
} }
@@ -288,7 +289,8 @@ class ZapstoreUpdater(
return null return null
} }
// Safe call needed: the offline flavor compiles against OkHttp 4, where body is nullable // Elvis needed: the offline flavor compiles against OkHttp 4, where body is nullable
@Suppress("USELESS_ELVIS")
val body = response.body ?: return null val body = response.body ?: return null
val contentLength = body.contentLength() val contentLength = body.contentLength()
val apkFile = File(context.cacheDir, "amber-update-${release.version}.apk") val apkFile = File(context.cacheDir, "amber-update-${release.version}.apk")
@@ -338,7 +340,7 @@ class ZapstoreUpdater(
} }
context.startActivity(intent) context.startActivity(intent)
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
delay(2000) delay(2.seconds)
downloadState.value = DownloadState.IDLE downloadState.value = DownloadState.IDLE
} }
} }
@@ -119,6 +119,7 @@ import com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.RandomInstance
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -1055,7 +1056,7 @@ fun LoginPage(
if (isValid.first != null) { if (isValid.first != null) {
keyPair = isValid.first!! keyPair = isValid.first!!
scope.launch { scope.launch {
delay(200) delay(200.milliseconds)
pageState.animateScrollToPage(1) pageState.animateScrollToPage(1)
} }
} else { } else {
@@ -1139,7 +1140,7 @@ fun LoginPage(
if (isValid.first != null) { if (isValid.first != null) {
keyPair = isValid.first!! keyPair = isValid.first!!
scope.launch { scope.launch {
delay(200) delay(200.milliseconds)
pageState.animateScrollToPage(1) pageState.animateScrollToPage(1)
} }
} else { } else {
@@ -1182,7 +1183,7 @@ fun LoginPage(
keyPair = isValid.first!! keyPair = isValid.first!!
keyboardController?.hide() keyboardController?.hide()
scope.launch { scope.launch {
delay(200) delay(200.milliseconds)
pageState.animateScrollToPage(1) pageState.animateScrollToPage(1)
} }
} else { } else {
@@ -1212,7 +1213,7 @@ fun LoginPage(
keyPair = isValid.first!! keyPair = isValid.first!!
keyboardController?.hide() keyboardController?.hide()
scope.launch { scope.launch {
delay(200) delay(200.milliseconds)
pageState.animateScrollToPage(1) pageState.animateScrollToPage(1)
} }
} else { } else {
@@ -7,12 +7,14 @@ import android.os.PersistableBundle
import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.platform.Clipboard
import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.Amber
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** How long a copied secret stays on the clipboard before it is cleared. */ /** 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 * 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 * 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 * still contains the copied secret, so anything the user copies afterwards is
* left untouched. * left untouched.
*/ */
@@ -42,19 +44,19 @@ suspend fun Clipboard.setSensitiveClip(
label: CharSequence, label: CharSequence,
text: CharSequence, text: CharSequence,
scope: CoroutineScope = Amber.instance.applicationIOScope, scope: CoroutineScope = Amber.instance.applicationIOScope,
clearAfterMillis: Long = SENSITIVE_CLIPBOARD_CLEAR_DELAY_MS, clearAfter: Duration = SENSITIVE_CLIPBOARD_CLEAR_DELAY,
) { ) {
setClipEntry(ClipEntry(newSensitivePlainText(label, text))) setClipEntry(ClipEntry(newSensitivePlainText(label, text)))
scheduleSensitiveClear(text, scope, clearAfterMillis) scheduleSensitiveClear(text, scope, clearAfter)
} }
private fun Clipboard.scheduleSensitiveClear( private fun Clipboard.scheduleSensitiveClear(
copiedValue: CharSequence, copiedValue: CharSequence,
scope: CoroutineScope, scope: CoroutineScope,
delayMillis: Long, clearAfter: Duration,
) { ) {
scope.launch { scope.launch {
delay(delayMillis) delay(clearAfter)
val currentText = getClipEntry()?.clipData?.let { clip -> val currentText = getClipEntry()?.clipData?.let { clip ->
if (clip.itemCount > 0) clip.getItemAt(0).text?.toString() else null 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.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -177,7 +178,7 @@ fun TranslationReportScreen(
Amber.instance.pendingTranslationReport.value = null Amber.instance.pendingTranslationReport.value = null
onLoading(false) onLoading(false)
onDismiss() onDismiss()
delay(10000) delay(10.seconds)
client.disconnect() client.disconnect()
} }
} }
@@ -89,6 +89,8 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.Base64 import java.util.Base64
import java.util.UUID import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -158,7 +160,7 @@ fun DefaultRelaysScreen(
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
if (!BuildFlavorChecker.isOfflineFlavor()) { if (!BuildFlavorChecker.isOfflineFlavor()) {
Amber.instance.checkForNewRelaysAndUpdateAllFilters() Amber.instance.checkForNewRelaysAndUpdateAllFilters()
delay(2000) delay(2.seconds)
Amber.instance.client.reconnect() Amber.instance.client.reconnect()
isLoading.value = false isLoading.value = false
} else { } else {
@@ -438,9 +440,9 @@ fun onAddRelay(
mapOf(addedWSS to filters), mapOf(addedWSS to filters),
) )
val canContinue = withTimeoutOrNull(30000) { val canContinue = withTimeoutOrNull(30.seconds) {
while (!canSendRequest) { while (!canSendRequest) {
delay(200) delay(200.milliseconds)
} }
true true
} }
@@ -469,7 +471,7 @@ fun onAddRelay(
success = client.publishAndConfirm(signedEvent, setOf(addedWSS)) success = client.publishAndConfirm(signedEvent, setOf(addedWSS))
if (!success) { if (!success) {
errorCount++ errorCount++
delay(1000) delay(1.seconds)
signedEvent = signer.signerSync.sign( signedEvent = signer.signerSync.sign(
TimeUtils.now(), TimeUtils.now(),
NostrConnectEvent.KIND, NostrConnectEvent.KIND,
@@ -483,7 +485,7 @@ fun onAddRelay(
AmberListenerSingleton.latestErrorMessages.clear() AmberListenerSingleton.latestErrorMessages.clear()
var count = 0 var count = 0
while (!filterResult && count < 10) { while (!filterResult && count < 10) {
delay(1000) delay(1.seconds)
count++ count++
} }
} else { } else {
@@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner
import kotlin.system.measureNanoTime import kotlin.system.measureNanoTime
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -77,7 +78,7 @@ class SignerProviderBenchmarkTest {
runBlocking { runBlocking {
val warmup = MutableStateFlow(true) val warmup = MutableStateFlow(true)
launch { launch {
delay(1) delay(1.milliseconds)
warmup.value = false warmup.value = false
} }
warmup.first { !it } warmup.first { !it }
@@ -87,7 +88,7 @@ class SignerProviderBenchmarkTest {
val state = MutableStateFlow(true) val state = MutableStateFlow(true)
runBlocking { runBlocking {
launch { launch {
delay(releaseAfterMs) delay(releaseAfterMs.milliseconds)
state.value = false state.value = false
} }
state.first { !it } state.first { !it }
@@ -6,6 +6,7 @@ import com.greenart7c3.nostrsigner.models.SignerType
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -344,8 +345,8 @@ class BunkerRequestUtilsTest {
var calls = 0 var calls = 0
val result = BunkerRequestUtils.retryWithBackoff( val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 3, maxRetries = 3,
initialDelayMs = 1L, initialDelay = 1.milliseconds,
maxDelayMs = 4L, maxDelay = 4.milliseconds,
) { ) {
calls++ calls++
true true
@@ -359,8 +360,8 @@ class BunkerRequestUtilsTest {
var calls = 0 var calls = 0
val result = BunkerRequestUtils.retryWithBackoff( val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 5, maxRetries = 5,
initialDelayMs = 1L, initialDelay = 1.milliseconds,
maxDelayMs = 4L, maxDelay = 4.milliseconds,
) { ) {
calls++ calls++
calls >= 3 calls >= 3
@@ -374,8 +375,8 @@ class BunkerRequestUtilsTest {
var calls = 0 var calls = 0
val result = BunkerRequestUtils.retryWithBackoff( val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 3, maxRetries = 3,
initialDelayMs = 1L, initialDelay = 1.milliseconds,
maxDelayMs = 4L, maxDelay = 4.milliseconds,
) { ) {
calls++ calls++
false false
@@ -389,8 +390,8 @@ class BunkerRequestUtilsTest {
var calls = 0 var calls = 0
val result = BunkerRequestUtils.retryWithBackoff( val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 1, maxRetries = 1,
initialDelayMs = 1L, initialDelay = 1.milliseconds,
maxDelayMs = 4L, maxDelay = 4.milliseconds,
) { ) {
calls++ calls++
false false
@@ -403,8 +404,8 @@ class BunkerRequestUtilsTest {
fun `retryWithBackoff with maxRetries 1 returns true on success`() = runBlocking { fun `retryWithBackoff with maxRetries 1 returns true on success`() = runBlocking {
val result = BunkerRequestUtils.retryWithBackoff( val result = BunkerRequestUtils.retryWithBackoff(
maxRetries = 1, maxRetries = 1,
initialDelayMs = 1L, initialDelay = 1.milliseconds,
maxDelayMs = 4L, maxDelay = 4.milliseconds,
) { true } ) { true }
assertTrue(result) assertTrue(result)
} }
@@ -415,8 +416,8 @@ class BunkerRequestUtilsTest {
var calls = 0 var calls = 0
BunkerRequestUtils.retryWithBackoff( BunkerRequestUtils.retryWithBackoff(
maxRetries = maxRetries, maxRetries = maxRetries,
initialDelayMs = 1L, initialDelay = 1.milliseconds,
maxDelayMs = 4L, maxDelay = 4.milliseconds,
) { ) {
calls++ calls++
false false