secant-android-wallet/ui-lib/src/main/java/co/electriccoin/zcash/work/SyncWorker.kt

129 lines
5.0 KiB
Kotlin

package co.electriccoin.zcash.work
import android.content.Context
import androidx.annotation.Keep
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.global.getInstance
import co.electriccoin.zcash.spackle.Twig
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.takeWhile
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.until
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlin.time.toJavaDuration
// TODO [#1249]: Add documentation and tests on background syncing
// TODO [#1249]: https://github.com/Electric-Coin-Company/zashi-android/issues/1249
@Keep
class SyncWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun doWork(): Result {
Twig.debug { "BG Sync: starting..." }
WalletCoordinator.getInstance(applicationContext).synchronizer
.flatMapLatest {
Twig.debug { "BG Sync: synchronizer: $it" }
it?.status?.combine(it.progress) { status, progress ->
StatusAndProgress(status, progress).also {
Twig.debug { "BG Sync: result: $it" }
}
} ?: emptyFlow()
}
.takeWhile {
it.status != Synchronizer.Status.DISCONNECTED &&
it.status != Synchronizer.Status.SYNCED
}
.collect()
Twig.debug { "BG Sync: terminating..." }
return Result.success()
}
companion object {
private val SYNC_PERIOD = 24.hours
private val SYNC_DAY_SHIFT = 1.days // Move to tomorrow
private val SYNC_START_TIME_HOURS = 3.hours // Start around 3 a.m. at night
private val SYNC_START_TIME_MINUTES = 60.minutes // Randomize with minutes until 4 a.m.
fun newWorkRequest(): PeriodicWorkRequest {
val targetTimeDiff = calculateTargetTimeDifference()
Twig.debug { "BG Sync: necessary trigger delay time: $targetTimeDiff" }
val constraints =
Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
// TODO [#1258]: Consider using flexInterval in BG sync trigger planning
// TODO [#1258]: https://github.com/Electric-Coin-Company/zashi-android/issues/1258
return PeriodicWorkRequestBuilder<SyncWorker>(SYNC_PERIOD.toJavaDuration())
.setConstraints(constraints)
.setInitialDelay(targetTimeDiff.toJavaDuration())
.build()
}
private fun calculateTargetTimeDifference(): Duration {
val currentTimeZone: TimeZone = TimeZone.currentSystemDefault()
val now: Instant = Clock.System.now()
val targetTime =
now
.plus(SYNC_DAY_SHIFT)
.toLocalDateTime(currentTimeZone)
.date
.atTime(
hour = SYNC_START_TIME_HOURS.inWholeHours.toInt(),
// Even though the WorkManager will trigger the work approximately at the set time, it's
// better to randomize time in 3-4 a.m. This generates a number between 0 (inclusive) and 60
// (exclusive)
minute = Random.nextInt(0, SYNC_START_TIME_MINUTES.inWholeMinutes.toInt())
)
Twig.debug { "BG Sync: calculated target time: ${targetTime.time}" }
return now.until(
other = targetTime.toInstant(currentTimeZone),
unit = DateTimeUnit.MILLISECOND,
timeZone = currentTimeZone
).toDuration(DurationUnit.MILLISECONDS)
}
}
}
// Enhancement to this implementation would be returning a better status information
private data class StatusAndProgress(
val status: Synchronizer.Status,
val progress: PercentDecimal
)