Replace java.time APIs with kotlin(x).(date)time (#3001)

* Replace java.time APIs with kotlin(x).(date)time

kotlinx-datetime explicitly says in its README that it does not cover
i18n, so we have to stick with using their toJavaX converters wherever
we localise dates and times.

Yes, some of these replacements are... questionable. But I wanted to
try and maximise the replacement for now.

* Bump kotlinx-datetime to 0.8.0

* More kotlin.time/kotlinx-datetime replacements

* Remove redundant init block & make attributes val

* Replace new use of java.time.Instant
This commit is contained in:
MajorTanya
2026-08-02 12:58:25 +02:00
committed by GitHub
parent 0648e2eaaf
commit 6d69903a56
55 changed files with 365 additions and 268 deletions
@@ -18,7 +18,7 @@ import tachiyomi.core.common.storage.displayablePath
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.milliseconds
class BackupNotifier(private val context: Context) {
@@ -149,10 +149,8 @@ class BackupNotifier(private val context: Context) {
val timeString = context.stringResource(
MR.strings.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time),
),
time.milliseconds.inWholeMinutes,
time.milliseconds.inWholeSeconds - (time.milliseconds.inWholeMinutes * 60),
)
with(completeNotificationBuilder) {
@@ -33,9 +33,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.Date
import java.util.Locale
import kotlin.time.Clock
class BackupCreator(
private val context: Context,
@@ -108,7 +108,7 @@ class BackupCreator(
BackupFileValidator(context).validate(fileUri)
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp.set(Instant.now().toEpochMilli())
backupPreferences.lastAutoBackupTimestamp.set(Clock.System.now().toEpochMilliseconds())
}
return fileUri.toString()
@@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import tachiyomi.data.Database
import tachiyomi.data.MemoColumnAdapter
import tachiyomi.data.UpdateStrategyColumnAdapter
@@ -23,9 +25,9 @@ import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date
import kotlin.math.max
import kotlin.time.Clock
class MangaRestorer(
private val database: Database = Injekt.get(),
@@ -38,13 +40,9 @@ class MangaRestorer(
fetchInterval: FetchInterval = Injekt.get(),
) {
private var now = ZonedDateTime.now()
private var currentFetchWindow = fetchInterval.getWindow(now)
init {
now = ZonedDateTime.now()
currentFetchWindow = fetchInterval.getWindow(now)
}
private val timeZone = TimeZone.currentSystemDefault()
private val now = Clock.System.now().toLocalDateTime(timeZone)
private val currentFetchWindow = fetchInterval.getWindow(now.date, timeZone)
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
val urlsBySource = database.mangasQueries
@@ -286,7 +284,7 @@ class MangaRestorer(
restoreTracking(manga, tracks)
restoreHistory(manga, history)
restoreExcludedScanlators(manga, excludedScanlators)
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
updateManga.awaitUpdateFetchInterval(manga, timeZone, now, currentFetchWindow)
return manga
}
@@ -35,6 +35,8 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
@@ -64,14 +66,13 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.time.Instant
import java.time.ZonedDateTime
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.time.Clock
@OptIn(ExperimentalAtomicApi::class)
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
@@ -108,7 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
setForegroundSafely()
libraryPreferences.lastUpdatedTimestamp.set(Instant.now().toEpochMilli())
libraryPreferences.lastUpdatedTimestamp.set(Clock.System.now().toEpochMilliseconds())
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
addMangaToQueue(categoryId)
@@ -167,7 +168,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = libraryPreferences.autoUpdateMangaRestrictions.get()
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now())
val timeZone = TimeZone.currentSystemDefault()
val (_, fetchWindowUpperBound) = fetchInterval.getWindow(
Clock.System.now().toLocalDateTime(timeZone).date,
timeZone,
)
mangaToUpdate = listToUpdate
.filter {
@@ -234,7 +239,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false)
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
val timeZone = TimeZone.currentSystemDefault()
val fetchWindow = fetchInterval.getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
coroutineScope {
mangaToUpdate.groupBy { it.manga.source }.values
@@ -24,7 +24,7 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.time.Instant
import kotlin.time.Clock
class ImageSaver(
val context: Context,
@@ -85,7 +85,7 @@ class ImageSaver(
MediaStore.MediaColumns.RELATIVE_PATH to relativePath,
MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename,
MediaStore.MediaColumns.MIME_TYPE to type.mime,
MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond,
MediaStore.MediaColumns.DATE_MODIFIED to Clock.System.now().epochSeconds,
)
val picture = findUriOrDefault(relativePath, filename) {
@@ -15,6 +15,9 @@ import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
@@ -25,10 +28,8 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Instant
import tachiyomi.domain.track.model.Track as DomainTrack
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
@@ -326,11 +327,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
val dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateValue), ZoneId.systemDefault())
val dateTime = Instant.fromEpochMilliseconds(dateValue).toLocalDateTime(TimeZone.currentSystemDefault())
return buildJsonObject {
put("year", dateTime.year)
put("month", dateTime.monthValue)
put("day", dateTime.dayOfMonth)
put("month", dateTime.month.number)
put("day", dateTime.day)
}
}
@@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.serialization.Serializable
import java.time.LocalDate
import java.time.ZoneId
@Serializable
data class ALFuzzyDate(
@@ -11,10 +12,9 @@ data class ALFuzzyDate(
val day: Int?,
) {
fun toEpochMilli(): Long = try {
LocalDate.of(year!!, month!!, day!!)
.atStartOfDay(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
LocalDate(year!!, month!!, day!!)
.atStartOfDayIn(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
} catch (_: Exception) {
0L
}
@@ -49,7 +49,7 @@ import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import kotlin.time.Clock
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
class BrowseSourceViewModel(
@@ -226,7 +226,7 @@ class BrowseSourceViewModel(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Instant.now().toEpochMilli()
false -> Clock.System.now().toEpochMilliseconds()
},
)
@@ -56,7 +56,6 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.lang.toLocalDate
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
@@ -66,6 +65,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.i18n.stringResource
@@ -85,9 +86,8 @@ import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import kotlin.time.Clock
import kotlin.time.Instant
data class TrackInfoDialogHomeScreen(
private val mangaId: Long,
@@ -513,20 +513,20 @@ private data class TrackDateSelectorScreen(
@Transient
private val selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val targetDate = Instant.ofEpochMilli(utcTimeMillis).toLocalDate(ZoneOffset.UTC)
val targetDate = Instant.fromEpochMilliseconds(utcTimeMillis).toLocalDateTime(TimeZone.UTC)
// Disallow future dates
if (targetDate > LocalDate.now(ZoneOffset.UTC)) return false
if (targetDate > Clock.System.now().toLocalDateTime(TimeZone.UTC)) return false
return when {
// Disallow setting start date after finish date
start && track.finishDate > 0 -> {
val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC)
val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC)
targetDate <= finishDate
}
// Disallow setting finish date before start date
!start && track.startDate > 0 -> {
val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC)
val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC)
startDate <= targetDate
}
else -> {
@@ -537,17 +537,17 @@ private data class TrackDateSelectorScreen(
override fun isSelectableYear(year: Int): Boolean {
// Disallow future years
if (year > LocalDate.now(ZoneOffset.UTC).year) return false
if (year > Clock.System.now().toLocalDateTime(TimeZone.UTC).year) return false
return when {
// Disallow setting start year after finish year
start && track.finishDate > 0 -> {
val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC)
val finishDate = Instant.fromEpochMilliseconds(track.finishDate).toLocalDateTime(TimeZone.UTC)
year <= finishDate.year
}
// Disallow setting finish year before start year
!start && track.startDate > 0 -> {
val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC)
val startDate = Instant.fromEpochMilliseconds(track.startDate).toLocalDateTime(TimeZone.UTC)
startDate.year <= year
}
else -> {
@@ -618,14 +618,14 @@ private data class TrackDateSelectorScreen(
get() {
val millis = (if (start) track.startDate else track.finishDate)
.takeIf { it != 0L }
?: Instant.now().toEpochMilli()
return millis.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
?: Clock.System.now().toEpochMilliseconds()
return millis.convertEpochMillisZone(TimeZone.currentSystemDefault(), TimeZone.UTC)
}
// In UTC
fun setDate(millis: Long) {
// Convert to local time
val localMillis = millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault())
val localMillis = millis.convertEpochMillisZone(TimeZone.UTC, TimeZone.currentSystemDefault())
viewModelScope.launchNonCancellable {
if (start) {
tracker.setRemoteStartDate(track.toDbTrack(), localMillis)
@@ -76,8 +76,9 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.util.Date
import kotlin.getValue
import kotlin.time.Clock
/**
* Presenter used by the activity to perform background operations.
@@ -582,7 +583,7 @@ class ReaderViewModel @JvmOverloads constructor(
}
fun restartReadTimer() {
chapterReadStartTime = Instant.now().toEpochMilli()
chapterReadStartTime = Clock.System.now().toEpochMilliseconds()
}
/**
@@ -31,6 +31,9 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.TriState
@@ -49,7 +52,7 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import kotlin.time.Clock
class UpdatesViewModel(
private val sourceManager: SourceManager = Injekt.get(),
@@ -77,7 +80,7 @@ class UpdatesViewModel(
init {
viewModelScope.launchIO {
// Set date limit for recent chapters
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
val limit = Clock.System.now().minus(3, DateTimeUnit.MONTH, TimeZone.currentSystemDefault())
combine(
// needed for SQL filters (unread, started, bookmarked, etc)
@@ -10,12 +10,14 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.datetime.TimeZone
import kotlinx.datetime.offsetAt
import kotlinx.datetime.toLocalDateTime
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.lang.withUIContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.OffsetDateTime
import java.time.ZoneId
import kotlin.time.Clock
class CrashLogUtil(
private val context: Context,
@@ -35,12 +37,14 @@ class CrashLogUtil(
val uri = file.getUriCompat(context)
context.startActivity(uri.toShareIntent(context, "text/plain"))
} catch (e: Throwable) {
} catch (_: Throwable) {
withUIContext { context.toast("Failed to get logs") }
}
}
fun getDebugInfo(): String {
val now = Clock.System.now()
val tz = TimeZone.currentSystemDefault()
return """
App ID: ${BuildConfig.APPLICATION_ID}
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME})
@@ -51,7 +55,7 @@ class CrashLogUtil(
Device name: ${Build.DEVICE} (${Build.PRODUCT})
Device model: ${Build.MODEL}
WebView: ${WebViewUtil.getVersion(context)}
Current time: ${OffsetDateTime.now(ZoneId.systemDefault())}
Current time: ${now.toLocalDateTime(tz)}${tz.offsetAt(now)}
""".trimIndent()
}
@@ -9,12 +9,12 @@ import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
import java.time.Instant
import kotlin.time.Clock
fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga {
if (isLocal()) return this
return if (coverCache.deleteFromCache(this, true) > 0) {
copy(coverLastModified = Instant.now().toEpochMilli())
copy(coverLastModified = Clock.System.now().toEpochMilliseconds())
} else {
this
}
@@ -1,23 +1,29 @@
package eu.kanade.tachiyomi.util.lang
import android.content.Context
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import java.text.DateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.math.absoluteValue
import kotlin.time.Clock
import kotlin.time.Instant
fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String {
val date = dateTimeFormatter.format(this)
val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this)
val javaLocalDateTime = this.toJavaLocalDateTime()
val date = dateTimeFormatter.format(javaLocalDateTime)
val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(javaLocalDateTime)
return "$date $time"
}
@@ -26,21 +32,21 @@ fun Date.toTimestampString(): String {
}
fun Long.convertEpochMillisZone(
from: ZoneId,
to: ZoneId,
from: TimeZone,
to: TimeZone,
): Long {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), from)
.atZone(to)
.toInstant()
.toEpochMilli()
return Instant.fromEpochMilliseconds(this)
.toLocalDateTime(from)
.toInstant(to)
.toEpochMilliseconds()
}
fun Long.toLocalDate(): LocalDate {
return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
return Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()).date
}
fun Instant.toLocalDate(zoneId: ZoneId = ZoneId.systemDefault()): LocalDate {
return LocalDate.ofInstant(this, zoneId)
fun Long.toJavaLocalDate(): java.time.LocalDate {
return this.toLocalDate().toJavaLocalDate()
}
fun LocalDate.toRelativeString(
@@ -49,23 +55,25 @@ fun LocalDate.toRelativeString(
dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT),
): String {
if (!relative) {
return dateFormat.format(this)
return dateFormat.format(this.toJavaLocalDate())
}
val now = LocalDate.now()
val difference = ChronoUnit.DAYS.between(this, now)
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val difference = (today - this).days
return when {
difference < -7 -> dateFormat.format(this)
difference < -7 -> dateFormat.format(this.toJavaLocalDate())
difference < 0 -> context.pluralStringResource(
MR.plurals.upcoming_relative_time,
difference.toInt().absoluteValue,
difference.toInt().absoluteValue,
difference.absoluteValue,
difference.absoluteValue,
)
difference < 1 -> context.stringResource(MR.strings.relative_time_today)
difference < 7 -> context.pluralStringResource(
MR.plurals.relative_time,
difference.toInt(),
difference.toInt(),
difference,
difference,
)
else -> dateFormat.format(this)
else -> dateFormat.format(this.toJavaLocalDate())
}
}