Migrate to kotlinx-datetime

resolves #4240

On the whole this was rather easy as the kotlinx-datetime API seems to be based on Java's.

Migrate from java.time to kotlinx-datetime
A further step to make the core part of StreetComplete platform independent (#1892), usages of `java.time` classes should be replaced with kotlinx-datetime.
I have had a cursory look at v0.4.0 so far and it looks stable enough now, though some convenience functions are missing.

**Steps to do**
- add [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) as dependency
- Ctrl+F for usages of `java.time` in the project
- replace usages with equivalent classes from kotlinx-datetime
- (you'll notice, a lot of convenience functions are missing, so add those as extension functions to `util/ktx/*.kt` instead of littering the code with unwieldy constructs)

**Note**
There is no 100% guarantee that every use case java.time is used in StreetComplete can be covered with kotlinx-datetime 0.4.0, after all it is still marked as "experimental" and I have had only a cursory look. But it looks quite complete by now.

hey @westnordost,
I want to pick this up. if it is still in the Unassigned stage.
It is not assigned. Thank you! Or well, @matkoniecz assigned it to you now :-)
I'll just unassign you in case someone else would like to do this. It can be re-assigned when you or someone else actually starts to work on it.

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3c0920cae44..d5c9a218c04 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -149,6 +149,9 @@ dependencies {
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinxCoroutinesVersion")

+    // Date/time
+    api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
+
     // scheduling background jobs
     implementation("androidx.work:work-runtime:2.7.1")

diff --git a/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt b/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt
index bca93152e1b..d45f9e388c5 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/StreetCompleteApplication.kt
@@ -48,6 +48,7 @@ import de.westnordost.streetcomplete.util.CrashReportExceptionHandler
 import de.westnordost.streetcomplete.util.getSelectedLocale
 import de.westnordost.streetcomplete.util.getSystemLocales
 import de.westnordost.streetcomplete.util.ktx.addedToFront
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.setDefaultLocales
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
@@ -58,7 +59,6 @@ import org.koin.android.ext.android.inject
 import org.koin.android.ext.koin.androidContext
 import org.koin.androidx.workmanager.koin.workManagerFactory
 import org.koin.core.context.startKoin
-import java.lang.System.currentTimeMillis
 import java.util.concurrent.TimeUnit

 class StreetCompleteApplication : Application() {
@@ -131,7 +131,7 @@ class StreetCompleteApplication : Application() {

         applicationScope.launch {
             preloader.preload()
-            editHistoryController.deleteSyncedOlderThan(currentTimeMillis() - ApplicationConstants.MAX_UNDO_HISTORY_AGE)
+            editHistoryController.deleteSyncedOlderThan(nowAsEpochMilliseconds() - ApplicationConstants.MAX_UNDO_HISTORY_AGE)
         }

         enqueuePeriodicCleanupWork()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt b/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt
index d983af72817..f99ff9065e0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/Cleaner.kt
@@ -6,7 +6,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataController
 import de.westnordost.streetcomplete.data.osmnotes.NoteController
 import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry
 import de.westnordost.streetcomplete.util.ktx.format
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 /** Deletes old unused data in the background */
 class Cleaner(
@@ -15,15 +15,15 @@ class Cleaner(
     private val questTypeRegistry: QuestTypeRegistry
 ) {
     fun clean() {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

-        val oldDataTimestamp = currentTimeMillis() - ApplicationConstants.DELETE_OLD_DATA_AFTER
+        val oldDataTimestamp = nowAsEpochMilliseconds() - ApplicationConstants.DELETE_OLD_DATA_AFTER
         noteController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS)
         mapDataController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS)
         /* do this after cleaning map data and notes, because some metadata rely on map data */
         questTypeRegistry.forEach { it.deleteMetadataOlderThan(oldDataTimestamp) }

-        Log.i(TAG, "Cleaning took ${((currentTimeMillis() - time) / 1000.0).format(1)}s")
+        Log.i(TAG, "Cleaning took ${((nowAsEpochMilliseconds() - time) / 1000.0).format(1)}s")
     }

     companion object {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt
index cd6872b77b3..85ffe2e5b17 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/Preloader.kt
@@ -4,11 +4,11 @@ import android.util.Log
 import de.westnordost.countryboundaries.CountryBoundaries
 import de.westnordost.osmfeatures.FeatureDictionary
 import de.westnordost.streetcomplete.util.ktx.format
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import java.lang.System.currentTimeMillis
 import java.util.concurrent.FutureTask

 /** Initialize certain singleton classes used elsewhere throughout the app in the background */
@@ -18,7 +18,7 @@ class Preloader(
 ) {

     suspend fun preload() {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         coroutineScope {
             // country boundaries are necessary latest for when a quest is opened or on a download
             launch { preloadCountryBoundaries() }
@@ -27,20 +27,20 @@ class Preloader(
             launch { preloadFeatureDictionary() }
         }

-        Log.i(TAG, "Preloading data took ${((currentTimeMillis() - time) / 1000.0).format(1)}s")
+        Log.i(TAG, "Preloading data took ${((nowAsEpochMilliseconds() - time) / 1000.0).format(1)}s")
     }

     private suspend fun preloadFeatureDictionary() = withContext(Dispatchers.IO) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         featuresDictionaryFuture.run()
-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Loaded features dictionary in ${seconds.format(1)}s")
     }

     private suspend fun preloadCountryBoundaries() = withContext(Dispatchers.IO) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         countryBoundariesFuture.run()
-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Loaded country boundaries in ${seconds.format(1)}s")
     }

diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt
index 77380dd5819..197d75d2e40 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/download/Downloader.kt
@@ -9,12 +9,12 @@ import de.westnordost.streetcomplete.data.maptiles.MapTilesDownloader
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataDownloader
 import de.westnordost.streetcomplete.data.osmnotes.NotesDownloader
 import de.westnordost.streetcomplete.util.ktx.format
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.math.area
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
-import java.lang.System.currentTimeMillis
 import kotlin.math.max

 /** Downloads all the things */
@@ -36,7 +36,7 @@ class Downloader(
         }
         Log.i(TAG, "Starting download ($sqkm km², bbox: $bboxString)")

-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         mutex.withLock {
             coroutineScope {
@@ -48,13 +48,13 @@ class Downloader(
         }
         putDownloadedAlready(tiles)

-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Finished download ($sqkm km², bbox: $bboxString) in ${seconds.format(1)}s")
     }

     private fun hasDownloadedAlready(tiles: TilesRect): Boolean {
         val freshTime = ApplicationConstants.REFRESH_DATA_AFTER
-        val ignoreOlderThan = max(0, currentTimeMillis() - freshTime)
+        val ignoreOlderThan = max(0, nowAsEpochMilliseconds() - freshTime)
         return downloadedTilesDb.get(tiles, ignoreOlderThan).contains(DownloadedTilesType.ALL)
     }

diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt b/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt
index 83bd5b573ad..99bd34bbd1d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/download/strategy/AVariableRadiusStrategy.kt
@@ -10,6 +10,7 @@ import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect
 import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataController
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.math.area
 import de.westnordost.streetcomplete.util.math.enclosingBoundingBox
 import kotlinx.coroutines.Dispatchers
@@ -79,7 +80,7 @@ abstract class AVariableRadiusStrategy(
     /** return if data in the given tiles rect that hasn't been downloaded yet */
     private suspend fun hasMissingDataFor(tilesRect: TilesRect): Boolean {
         val dataExpirationTime = ApplicationConstants.REFRESH_DATA_AFTER
-        val ignoreOlderThan = max(0, System.currentTimeMillis() - dataExpirationTime)
+        val ignoreOlderThan = max(0, nowAsEpochMilliseconds() - dataExpirationTime)
         val downloadedTiles =
             withContext(Dispatchers.IO) { downloadedTilesDao.get(tilesRect, ignoreOlderThan) }
         return !downloadedTiles.contains(DownloadedTilesType.ALL)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDao.kt
index 60de1d28569..001b78c76f0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDao.kt
@@ -6,14 +6,14 @@ import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesTable.Co
 import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesTable.Columns.X
 import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesTable.Columns.Y
 import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesTable.NAME
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 /** Keeps info in which areas things have been downloaded already in a tile grid */
 class DownloadedTilesDao(private val db: Database) {

     /** Persist that the given type has been downloaded in every tile in the given tile range  */
     fun put(tilesRect: TilesRect, typeName: String) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         db.replaceMany(NAME,
             arrayOf(X, Y, TYPE, DATE),
             tilesRect.asTilePosSequence().map { arrayOf<Any?>(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/edithistory/EditHistoryController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/edithistory/EditHistoryController.kt
index 38325135849..64c4f45855c 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/edithistory/EditHistoryController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/edithistory/EditHistoryController.kt
@@ -12,7 +12,7 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsController
 import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsSource
 import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestController
 import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestHidden
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.util.concurrent.CopyOnWriteArrayList

 /** All edits done by the user in one place: Edits made on notes, on map data, hidings of quests */
@@ -88,7 +88,7 @@ class EditHistoryController(
         getAll().firstOrNull { it.isUndoable }

     override fun getAll(): List<Edit> {
-        val maxAge = currentTimeMillis() - MAX_UNDO_HISTORY_AGE
+        val maxAge = nowAsEpochMilliseconds() - MAX_UNDO_HISTORY_AGE

         val result = ArrayList<Edit>()
         result += elementEditsController.getAll().filter { it.action !is IsRevertAction }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilter.kt b/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilter.kt
index 7a0cc119fd8..795f609cdfe 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilter.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilter.kt
@@ -6,8 +6,8 @@ import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.osm.getLastCheckDateKeys
 import de.westnordost.streetcomplete.osm.toCheckDate
 import de.westnordost.streetcomplete.util.ktx.toLocalDate
-import java.time.Instant
-import java.time.LocalDate
+import kotlinx.datetime.Instant
+import kotlinx.datetime.LocalDate

 sealed interface ElementFilter : Matcher<Element> {
     abstract override fun toString(): String
@@ -132,7 +132,7 @@ class TagNewerThan(key: String, dateFilter: DateFilter) : CompareTagAge(key, dat
 abstract class CompareTagAge(val key: String, val dateFilter: DateFilter) : ElementFilter {
     abstract fun compareTo(tagValue: LocalDate): Boolean
     override fun matches(obj: Element): Boolean {
-        if (compareTo(Instant.ofEpochMilli(obj.timestampEdited).toLocalDate())) return true
+        if (compareTo(Instant.fromEpochMilliseconds(obj.timestampEdited).toLocalDate())) return true
         return getLastCheckDateKeys(key)
             .mapNotNull { obj.tags[it]?.toCheckDate() }
             .any { compareTo(it) }
@@ -150,7 +150,7 @@ class ElementNewerThan(dateFilter: DateFilter) : CompareElementAge(dateFilter) {

 abstract class CompareElementAge(val dateFilter: DateFilter) : ElementFilter {
     abstract fun compareTo(tagValue: LocalDate): Boolean
-    override fun matches(obj: Element) = compareTo(Instant.ofEpochMilli(obj.timestampEdited).toLocalDate())
+    override fun matches(obj: Element) = compareTo(Instant.fromEpochMilliseconds(obj.timestampEdited).toLocalDate())
 }

 class CombineFilters(vararg val filters: ElementFilter) : ElementFilter {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/RelativeDate.kt b/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/RelativeDate.kt
index 59f77af423e..fe974cbb018 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/RelativeDate.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/elementfilter/filters/RelativeDate.kt
@@ -1,7 +1,11 @@
 package de.westnordost.streetcomplete.data.elementfilter.filters

-import java.time.LocalDate
-import java.time.LocalDateTime
+import de.westnordost.streetcomplete.util.ktx.minusInSystemTimeZone
+import de.westnordost.streetcomplete.util.ktx.now
+import de.westnordost.streetcomplete.util.ktx.plusInSystemTimeZone
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
 import kotlin.math.absoluteValue

 interface DateFilter {
@@ -14,10 +18,10 @@ class RelativeDate(val deltaDays: Float) : DateFilter {
         val now = LocalDateTime.now()
         val plusHours = (deltaDays * MULTIPLIER * 24).toLong()
         val relativeDateTime = (
-            if (plusHours > 0) now.plusHours(plusHours)
-            else now.minusHours(plusHours.absoluteValue)
+            if (plusHours > 0) now.plusInSystemTimeZone(plusHours, DateTimeUnit.HOUR)
+            else now.minusInSystemTimeZone(plusHours.absoluteValue, DateTimeUnit.HOUR)
         )
-        return relativeDateTime.toLocalDate()
+        return relativeDateTime.date
     }

     override fun toString() = "$deltaDays days"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt
index bd9d83d472f..e6141a7a163 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt
@@ -6,6 +6,7 @@ import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect
 import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
 import de.westnordost.streetcomplete.screens.main.map.VectorTileProvider
 import de.westnordost.streetcomplete.util.ktx.format
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
@@ -19,7 +20,6 @@ import okhttp3.Request
 import okhttp3.Response
 import okhttp3.internal.Version
 import java.io.IOException
-import java.lang.System.currentTimeMillis
 import kotlin.coroutines.resume

 class MapTilesDownloader(
@@ -35,7 +35,7 @@ class MapTilesDownloader(
         var failureCount = 0
         var downloadedSize = 0
         var cachedSize = 0
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         coroutineScope {
             for (tile in getDownloadTileSequence(bbox)) {
@@ -56,7 +56,7 @@ class MapTilesDownloader(
                 }
             }
         }
-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         val failureText = if (failureCount > 0) ". $failureCount tiles failed to download" else ""
         Log.i(TAG, "Downloaded $tileCount tiles (${downloadedSize / 1000}kB downloaded, ${cachedSize / 1000}kB already cached) in ${seconds.format(1)}s$failureText")
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/ElementEditsController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/ElementEditsController.kt
index 8c0e0129087..58ee6098f38 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/ElementEditsController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/ElementEditsController.kt
@@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.osm.mapdata.ElementType
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataUpdates
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.util.concurrent.CopyOnWriteArrayList

 class ElementEditsController(
@@ -28,7 +28,7 @@ class ElementEditsController(
         source: String,
         action: ElementEditAction
     ) {
-        add(ElementEdit(0, type, element.type, element.id, element, geometry, source, currentTimeMillis(), false, action))
+        add(ElementEdit(0, type, element.type, element.id, element, geometry, source, nowAsEpochMilliseconds(), false, action))
     }

     fun get(id: Long): ElementEdit? =
@@ -99,7 +99,7 @@ class ElementEditsController(
             // need to delete the original edit from history because this should not be undoable anymore
             delete(edit)
             // ... and add a new revert to the queue
-            add(ElementEdit(0, edit.type, edit.elementType, edit.elementId, edit.originalElement, edit.originalGeometry, edit.source, currentTimeMillis(), false, action.createReverted()))
+            add(ElementEdit(0, edit.type, edit.elementType, edit.elementId, edit.originalElement, edit.originalGeometry, edit.source, nowAsEpochMilliseconds(), false, action.createReverted()))
         }
         // not uploaded yet
         else {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/create/CreateNodeAction.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/create/CreateNodeAction.kt
index dee0a0ec88a..06fff100b72 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/create/CreateNodeAction.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/create/CreateNodeAction.kt
@@ -9,8 +9,8 @@ import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataChanges
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataRepository
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.Serializable
-import java.time.Instant

 /** Action that creates a (free-floating) node. */
 @Serializable
@@ -27,7 +27,7 @@ data class CreateNodeAction(
         mapDataRepository: MapDataRepository,
         idProvider: ElementIdProvider
     ): MapDataChanges {
-        val node = Node(idProvider.nextNodeId(), position, tags, 1, Instant.now().toEpochMilli())
+        val node = Node(idProvider.nextNodeId(), position, tags, 1, nowAsEpochMilliseconds())
         return MapDataChanges(creations = listOf(node))
     }

diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/DeletePoiNodeAction.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/DeletePoiNodeAction.kt
index 9cf14f414f4..dcc73083158 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/DeletePoiNodeAction.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/DeletePoiNodeAction.kt
@@ -9,6 +9,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataChanges
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataRepository
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
 import de.westnordost.streetcomplete.data.upload.ConflictException
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.Serializable

 /** Action that deletes a POI node.
@@ -47,7 +48,7 @@ object DeletePoiNodeAction : ElementEditAction, IsActionRevertable {
         else {
             MapDataChanges(modifications = listOf(node.copy(
                 tags = emptyMap(),
-                timestampEdited = System.currentTimeMillis()
+                timestampEdited = nowAsEpochMilliseconds()
             )))
         }
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/RevertDeletePoiNodeAction.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/RevertDeletePoiNodeAction.kt
index 199590880e5..9d43ad9b871 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/RevertDeletePoiNodeAction.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/delete/RevertDeletePoiNodeAction.kt
@@ -9,8 +9,8 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataChanges
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataRepository
 import de.westnordost.streetcomplete.data.osm.mapdata.Node
 import de.westnordost.streetcomplete.data.upload.ConflictException
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.Serializable
-import java.lang.System.currentTimeMillis

 /** Action that restores a POI node to the previous state before deletion/clearing of tags
  */
@@ -36,7 +36,7 @@ object RevertDeletePoiNodeAction : ElementEditAction, IsRevertAction {

         val newElement = originalElement.copy(
             version = newVersion,
-            timestampEdited = currentTimeMillis()
+            timestampEdited = nowAsEpochMilliseconds()
         )
         return MapDataChanges(modifications = listOf(newElement))
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/split_way/SplitWayAction.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/split_way/SplitWayAction.kt
index 93cd108b38f..ecb9e4287ce 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/split_way/SplitWayAction.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/split_way/SplitWayAction.kt
@@ -18,8 +18,8 @@ import de.westnordost.streetcomplete.util.ktx.findNext
 import de.westnordost.streetcomplete.util.ktx.findPrevious
 import de.westnordost.streetcomplete.util.ktx.firstAndLast
 import de.westnordost.streetcomplete.util.ktx.indexOfMaxBy
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.Serializable
-import java.lang.System.currentTimeMillis

 /** Action that performs a split on a way.
  *
@@ -76,7 +76,7 @@ data class SplitWayAction(private val splits: List<SplitPolylineAtPosition>) : E
                     splitAtIndices.add(split.index + insertedNodeCount)
                 }
                 is SplitWayAtLinePosition -> {
-                    val splitNode = Node(idProvider.nextNodeId(), split.pos, emptyMap(), 1, currentTimeMillis())
+                    val splitNode = Node(idProvider.nextNodeId(), split.pos, emptyMap(), 1, nowAsEpochMilliseconds())
                     createdNodes.add(splitNode)

                     val nodeIndex = split.index2 + insertedNodeCount
@@ -198,7 +198,7 @@ private fun getUpdatedRelations(
         }
         result.add(relation.copy(
             members = updatedRelationMembers,
-            timestampEdited = currentTimeMillis()
+            timestampEdited = nowAsEpochMilliseconds()
         ))
     }
     return result
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesXt.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesXt.kt
index f3f9611aa81..11e18fd26e9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesXt.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesXt.kt
@@ -3,7 +3,7 @@ package de.westnordost.streetcomplete.data.osm.edits.update_tags
 import de.westnordost.streetcomplete.data.osm.mapdata.Element
 import de.westnordost.streetcomplete.data.upload.ConflictException
 import de.westnordost.streetcomplete.util.ktx.copy
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 fun Element.changesApplied(changes: StringMapChanges): Element {
     val tags = tags.toMutableMap()
@@ -25,6 +25,6 @@ fun Element.changesApplied(changes: StringMapChanges): Element {
     }
     return this.copy(
         tags = tags,
-        timestampEdited = currentTimeMillis()
+        timestampEdited = nowAsEpochMilliseconds()
     )
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/LastEditTimeStore.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/LastEditTimeStore.kt
index e82f175b4d8..be03d213de0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/LastEditTimeStore.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/LastEditTimeStore.kt
@@ -3,12 +3,12 @@ package de.westnordost.streetcomplete.data.osm.edits.upload
 import android.content.SharedPreferences
 import androidx.core.content.edit
 import de.westnordost.streetcomplete.Prefs
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 class LastEditTimeStore(private val prefs: SharedPreferences) {

     fun touch() {
-        prefs.edit { putLong(Prefs.LAST_EDIT_TIME, currentTimeMillis()) }
+        prefs.edit { putLong(Prefs.LAST_EDIT_TIME, nowAsEpochMilliseconds()) }
     }

     fun get(): Long =
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt
index 956b6bb3090..91e1bd2abb8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/edits/upload/changesets/OpenChangesetsManager.kt
@@ -7,6 +7,7 @@ import de.westnordost.streetcomplete.data.osm.edits.ElementEditType
 import de.westnordost.streetcomplete.data.osm.edits.upload.LastEditTimeStore
 import de.westnordost.streetcomplete.data.osm.mapdata.MapDataApi
 import de.westnordost.streetcomplete.data.upload.ConflictException
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.util.Locale

 /** Manages the creation and reusage of changesets */
@@ -34,7 +35,7 @@ class OpenChangesetsManager(
     }

     fun closeOldChangesets() = synchronized(this) {
-        val timePassed = System.currentTimeMillis() - lastEditTimeStore.get()
+        val timePassed = nowAsEpochMilliseconds() - lastEditTimeStore.get()
         if (timePassed < CLOSE_CHANGESETS_AFTER_INACTIVITY_OF) return

         for (info in openChangesetsDB.getAll()) {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataApiImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataApiImpl.kt
index ab2b6b1d59d..b5c606a8856 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataApiImpl.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataApiImpl.kt
@@ -19,7 +19,8 @@ import de.westnordost.streetcomplete.data.download.ConnectionException
 import de.westnordost.streetcomplete.data.download.QueryTooBigException
 import de.westnordost.streetcomplete.data.upload.ConflictException
 import de.westnordost.streetcomplete.data.user.AuthorizationException
-import java.time.Instant
+import kotlinx.datetime.Instant
+import kotlinx.datetime.toJavaInstant
 import de.westnordost.osmapi.map.MapDataApi as OsmApiMapDataApi
 import de.westnordost.osmapi.map.changes.DiffElement as OsmApiDiffElement
 import de.westnordost.osmapi.map.data.BoundingBox as OsmApiBoundingBox
@@ -157,7 +158,7 @@ private fun Node.toOsmApiNode() = OsmNode(
     OsmLatLon(position.latitude, position.longitude),
     tags,
     null,
-    Instant.ofEpochMilli(timestampEdited)
+    Instant.fromEpochMilliseconds(timestampEdited).toJavaInstant()
 )

 private fun Way.toOsmApiWay() = OsmWay(
@@ -166,7 +167,7 @@ private fun Way.toOsmApiWay() = OsmWay(
     nodeIds,
     tags,
     null,
-    Instant.ofEpochMilli(timestampEdited)
+    Instant.fromEpochMilliseconds(timestampEdited).toJavaInstant()
 )

 private fun Relation.toOsmApiRelation() = OsmRelation(
@@ -175,7 +176,7 @@ private fun Relation.toOsmApiRelation() = OsmRelation(
     members.map { it.toOsmRelationMember() },
     tags,
     null,
-    Instant.ofEpochMilli(timestampEdited)
+    Instant.fromEpochMilliseconds(timestampEdited).toJavaInstant()
 )

 private fun RelationMember.toOsmRelationMember() = OsmRelationMember(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt
index 8e8c92bb203..73bc434edc9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt
@@ -7,7 +7,7 @@ import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometryCreator
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometryDao
 import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometryEntry
 import de.westnordost.streetcomplete.util.ktx.format
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.util.concurrent.CopyOnWriteArrayList

 /** Controller to access element data and its geometry and handle updates to it (from OSM API) */
@@ -53,7 +53,7 @@ class MapDataController internal constructor(
     /** update element data with [mapData] in the given [bbox] (fresh data from the OSM API has been
      *  downloaded) */
     fun putAllForBBox(bbox: BoundingBox, mapData: MutableMapData) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         val oldElementKeys: Set<ElementKey>
         val geometryEntries: Collection<ElementGeometryEntry>
@@ -82,7 +82,7 @@ class MapDataController internal constructor(

         Log.i(TAG,
             "Persisted ${geometryEntries.size} and deleted ${oldElementKeys.size} elements and geometries" +
-            " in ${((currentTimeMillis() - time) / 1000.0).format(1)}s"
+            " in ${((nowAsEpochMilliseconds() - time) / 1000.0).format(1)}s"
         )

         val mapDataWithGeometry = MutableMapDataWithGeometry(mapData, geometryEntries)
@@ -167,9 +167,9 @@ class MapDataController internal constructor(
     fun getGeometries(keys: Collection<ElementKey>): List<ElementGeometryEntry> = cache.getGeometries(keys, geometryDB::getAllEntries)

     fun getMapDataWithGeometry(bbox: BoundingBox): MutableMapDataWithGeometry {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         val result = cache.getMapDataWithGeometry(bbox)
-        Log.i(TAG, "Fetched ${result.size} elements and geometries in ${currentTimeMillis() - time}ms")
+        Log.i(TAG, "Fetched ${result.size} elements and geometries in ${nowAsEpochMilliseconds() - time}ms")

         return result
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt
index 03eefe908ff..349f67b395e 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MapDataDownloader.kt
@@ -4,11 +4,11 @@ import android.util.Log
 import de.westnordost.streetcomplete.ApplicationConstants
 import de.westnordost.streetcomplete.data.download.QueryTooBigException
 import de.westnordost.streetcomplete.util.ktx.format
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.math.enlargedBy
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.yield
-import java.lang.System.currentTimeMillis

 /** Takes care of downloading all note and osm quests */
 class MapDataDownloader(
@@ -16,7 +16,7 @@ class MapDataDownloader(
     private val mapDataController: MapDataController
 ) {
     suspend fun download(bbox: BoundingBox) = withContext(Dispatchers.IO) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         val mapData = MutableMapData()
         val expandedBBox = bbox.enlargedBy(ApplicationConstants.QUEST_FILTER_PADDING)
@@ -25,7 +25,7 @@ class MapDataDownloader(
            split up in several, so lets set the bbox back to the bbox of the complete download */
         mapData.boundingBox = expandedBBox

-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Downloaded ${mapData.nodes.size} nodes, ${mapData.ways.size} ways and ${mapData.relations.size} relations in ${seconds.format(1)}s")

         yield()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDao.kt
index 61c52e0e45e..c5c70c532f2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDao.kt
@@ -12,10 +12,10 @@ import de.westnordost.streetcomplete.data.osm.mapdata.NodeTable.Columns.TAGS
 import de.westnordost.streetcomplete.data.osm.mapdata.NodeTable.Columns.TIMESTAMP
 import de.westnordost.streetcomplete.data.osm.mapdata.NodeTable.Columns.VERSION
 import de.westnordost.streetcomplete.data.osm.mapdata.NodeTable.NAME
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
-import java.lang.System.currentTimeMillis

 /** Stores OSM nodes */
 class NodeDao(private val db: Database) {
@@ -32,7 +32,7 @@ class NodeDao(private val db: Database) {
     fun putAll(nodes: Collection<Node>) {
         if (nodes.isEmpty()) return

-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         db.replaceMany(NAME,
             arrayOf(ID, VERSION, LATITUDE, LONGITUDE, TAGS, TIMESTAMP, LAST_SYNC),
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt
index b3d0cc1d98a..5feec11cad9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDao.kt
@@ -12,10 +12,10 @@ import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.Columns.TYP
 import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.Columns.VERSION
 import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.NAME
 import de.westnordost.streetcomplete.data.osm.mapdata.RelationTables.NAME_MEMBERS
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
-import java.lang.System.currentTimeMillis

 /** Stores OSM relations */
 class RelationDao(private val db: Database) {
@@ -33,7 +33,7 @@ class RelationDao(private val db: Database) {
         if (relations.isEmpty()) return
         val idsString = relations.joinToString(",") { it.id.toString() }

-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         db.transaction {
             db.delete(NAME_MEMBERS, "$ID IN ($idsString)")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDao.kt
index faee17ecd05..84bd1bfb13d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDao.kt
@@ -10,10 +10,10 @@ import de.westnordost.streetcomplete.data.osm.mapdata.WayTables.Columns.TIMESTAM
 import de.westnordost.streetcomplete.data.osm.mapdata.WayTables.Columns.VERSION
 import de.westnordost.streetcomplete.data.osm.mapdata.WayTables.NAME
 import de.westnordost.streetcomplete.data.osm.mapdata.WayTables.NAME_NODES
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
-import java.lang.System.currentTimeMillis

 /** Stores OSM ways */
 class WayDao(private val db: Database) {
@@ -31,7 +31,7 @@ class WayDao(private val db: Database) {
         if (ways.isEmpty()) return
         val idsString = ways.joinToString(",") { it.id.toString() }

-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         db.transaction {
             db.delete(NAME_NODES, "$ID IN ($idsString)")
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt
index b300c07e539..5367a733bfd 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestController.kt
@@ -24,6 +24,7 @@ import de.westnordost.streetcomplete.quests.place_name.AddPlaceName
 import de.westnordost.streetcomplete.util.ktx.format
 import de.westnordost.streetcomplete.util.ktx.intersects
 import de.westnordost.streetcomplete.util.ktx.isInAny
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.math.contains
 import de.westnordost.streetcomplete.util.math.enclosingBoundingBox
 import de.westnordost.streetcomplete.util.math.enlargedBy
@@ -35,7 +36,6 @@ import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
-import java.lang.System.currentTimeMillis
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.FutureTask

@@ -73,7 +73,7 @@ class OsmQuestController internal constructor(
          *  OSM elements are updated, so the quests that reference that element need to be updated
          *  as well. */
         override fun onUpdated(updated: MapDataWithGeometry, deleted: Collection<ElementKey>) {
-            val time = currentTimeMillis()
+            val time = nowAsEpochMilliseconds()

             val deferredQuests = mutableListOf<Deferred<OsmQuest?>>()

@@ -93,7 +93,7 @@ class OsmQuestController internal constructor(
                 // quests that refer to elements that have been deleted shall be deleted
                 val deleteQuestKeys = db.getAllForElements(deleted).map { it.key }

-                val seconds = (currentTimeMillis() - time) / 1000.0
+                val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
                 Log.i(TAG, "Created ${quests.size} quests for ${updated.size} updated elements in ${seconds.format(1)}s")

                 obsoleteQuestKeys = getObsoleteQuestKeys(quests, previousQuests, deleteQuestKeys)
@@ -143,7 +143,7 @@ class OsmQuestController internal constructor(
         mapDataWithGeometry: MapDataWithGeometry,
         questTypes: Collection<OsmElementQuestType<*>>,
     ): Collection<OsmQuest> {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         val countryBoundaries = countryBoundariesFuture.get()

@@ -161,7 +161,7 @@ class OsmQuestController internal constructor(
                     Log.d(TAG, "$questTypeName: Skipped because it is disabled for this country")
                     emptyList()
                 } else {
-                    val questTime = currentTimeMillis()
+                    val questTime = nowAsEpochMilliseconds()
                     var questCount = 0
                     val mapDataToUse = if (questType is OsmFilterQuestType && !questType.filter.mayEvaluateToTrueWithNoTags)
                             onlyElementsWithTags
@@ -175,7 +175,7 @@ class OsmQuestController internal constructor(
                         questCount++
                     }

-                    val questSeconds = currentTimeMillis() - questTime
+                    val questSeconds = nowAsEpochMilliseconds() - questTime
                     Log.d(TAG, "$questTypeName: Found $questCount quests in ${questSeconds}ms")
                     questsForType
                 }
@@ -183,7 +183,7 @@ class OsmQuestController internal constructor(
         }
         val quests = runBlocking { deferredQuests.awaitAll().flatten() }

-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Created ${quests.size} quests for bbox in ${seconds.format(1)}s")

         return quests
@@ -233,12 +233,12 @@ class OsmQuestController internal constructor(
     }

     private fun updateQuests(questsNow: Collection<OsmQuest>, obsoleteQuestKeys: Collection<OsmQuestKey>) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         db.deleteAll(obsoleteQuestKeys)
         db.putAll(questsNow)

-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Persisted ${questsNow.size} new and removed ${obsoleteQuestKeys.size} already resolved quests in ${seconds.format(1)}s")
     }

diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDao.kt
index a41d99add57..196a2330eaf 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDao.kt
@@ -9,7 +9,7 @@ import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestsHiddenTable.Col
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestsHiddenTable.Columns.TIMESTAMP
 import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestsHiddenTable.NAME
 import de.westnordost.streetcomplete.data.quest.OsmQuestKey
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 /** Persists which osm quests should be hidden (because the user selected so) */
 class OsmQuestsHiddenDao(private val db: Database) {
@@ -55,7 +55,7 @@ private fun OsmQuestKey.toPairs() = listOf(
     ELEMENT_TYPE to elementType.name,
     ELEMENT_ID to elementId,
     QUEST_TYPE to questTypeName,
-    TIMESTAMP to currentTimeMillis()
+    TIMESTAMP to nowAsEpochMilliseconds()
 )

 private fun CursorPosition.toOsmQuestKey() = OsmQuestKey(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt
index 5a6eba8d3fb..9bfec14c054 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/AvatarsDownloader.kt
@@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.data.osmnotes
 import android.util.Log
 import de.westnordost.osmapi.user.UserApi
 import de.westnordost.streetcomplete.util.ktx.format
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.ktx.saveToFile
 import java.io.File
 import java.io.IOException
@@ -20,14 +21,14 @@ class AvatarsDownloader(
             return
         }

-        val time = System.currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         for (userId in userIds) {
             val avatarUrl = getProfileImageUrl(userId)
             if (avatarUrl != null) {
                 download(userId, avatarUrl)
             }
         }
-        val seconds = (System.currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Downloaded ${userIds.size} avatar images in ${seconds.format(1)}s")
     }

diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt
index c2b109ee087..8ad78dd6a44 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteController.kt
@@ -4,7 +4,7 @@ import android.util.Log
 import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.util.ktx.format
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.util.concurrent.CopyOnWriteArrayList

 /** Manages access to the notes storage */
@@ -25,7 +25,7 @@ class NoteController(

     /** Replace all notes in the given bounding box with the given notes */
     fun putAllForBBox(bbox: BoundingBox, notes: Collection<Note>) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         val oldNotesById = mutableMapOf<Long, Note>()
         val addedNotes = mutableListOf<Note>()
@@ -46,7 +46,7 @@ class NoteController(
             dao.deleteAll(oldNotesById.keys)
         }

-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Persisted ${addedNotes.size} and deleted ${oldNotesById.size} notes in ${seconds.format(1)}s")

         onUpdated(added = addedNotes, updated = updatedNotes, deleted = oldNotesById.keys)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt
index 98af3795f9a..768c5e77cb8 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NoteDao.kt
@@ -13,10 +13,10 @@ import de.westnordost.streetcomplete.data.osmnotes.NoteTable.Columns.LATITUDE
 import de.westnordost.streetcomplete.data.osmnotes.NoteTable.Columns.LONGITUDE
 import de.westnordost.streetcomplete.data.osmnotes.NoteTable.Columns.STATUS
 import de.westnordost.streetcomplete.data.osmnotes.NoteTable.NAME
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
-import java.lang.System.currentTimeMillis

 /** Stores OSM notes */
 class NoteDao(private val db: Database) {
@@ -43,7 +43,7 @@ class NoteDao(private val db: Database) {
                 it.timestampCreated,
                 it.timestampClosed,
                 Json.encodeToString(it.comments),
-                currentTimeMillis()
+                nowAsEpochMilliseconds()
             ) }
         )
     }
@@ -88,7 +88,7 @@ class NoteDao(private val db: Database) {
         CREATED to timestampCreated,
         CLOSED to timestampClosed,
         COMMENTS to Json.encodeToString(comments),
-        LAST_SYNC to currentTimeMillis()
+        LAST_SYNC to nowAsEpochMilliseconds()
     )

     private fun CursorPosition.toNote() = Note(
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt
index 8c10ec08270..6bf52fbfba7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/NotesDownloader.kt
@@ -3,10 +3,10 @@ package de.westnordost.streetcomplete.data.osmnotes
 import android.util.Log
 import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
 import de.westnordost.streetcomplete.util.ktx.format
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.yield
-import java.lang.System.currentTimeMillis

 /** Takes care of downloading notes and referenced avatar pictures into persistent storage */
 class NotesDownloader(
@@ -14,14 +14,14 @@ class NotesDownloader(
     private val noteController: NoteController
 ) {
     suspend fun download(bbox: BoundingBox) = withContext(Dispatchers.IO) {
-        val time = currentTimeMillis()
+        val time = nowAsEpochMilliseconds()

         val notes = notesApi
             .getAll(bbox, 10000, 0)
             // exclude invalid notes (#1338)
             .filter { it.comments.isNotEmpty() }

-        val seconds = (currentTimeMillis() - time) / 1000.0
+        val seconds = (nowAsEpochMilliseconds() - time) / 1000.0
         Log.i(TAG, "Downloaded ${notes.size} notes in ${seconds.format(1)}s")

         yield()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsController.kt
index 460ecaf475d..fb1438f63f9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsController.kt
@@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.ElementIdUpdate
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.osmnotes.Note
 import de.westnordost.streetcomplete.data.osmtracks.Trackpoint
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.util.concurrent.CopyOnWriteArrayList

 class NoteEditsController(
@@ -31,7 +31,7 @@ class NoteEditsController(
             action,
             text,
             imagePaths,
-            currentTimeMillis(),
+            nowAsEpochMilliseconds(),
             false,
             imagePaths.isNotEmpty(),
             track,
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDao.kt
index a0906424c6d..f04b4a354d2 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDao.kt
@@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.Database
 import de.westnordost.streetcomplete.data.osmnotes.notequests.NoteQuestsHiddenTable.Columns.NOTE_ID
 import de.westnordost.streetcomplete.data.osmnotes.notequests.NoteQuestsHiddenTable.Columns.TIMESTAMP
 import de.westnordost.streetcomplete.data.osmnotes.notequests.NoteQuestsHiddenTable.NAME
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 /** Persists which note ids should be hidden (because the user selected so) in the note quest */
 class NoteQuestsHiddenDao(private val db: Database) {
@@ -13,7 +13,7 @@ class NoteQuestsHiddenDao(private val db: Database) {
     fun add(noteId: Long) {
         db.insert(NAME, listOf(
             NOTE_ID to noteId,
-            TIMESTAMP to currentTimeMillis()
+            TIMESTAMP to nowAsEpochMilliseconds()
         ))
     }

diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/osmtracks/TracksApiImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/data/osmtracks/TracksApiImpl.kt
index e09bcb85738..3efd9900997 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/osmtracks/TracksApiImpl.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/osmtracks/TracksApiImpl.kt
@@ -12,9 +12,11 @@ import de.westnordost.osmapi.traces.GpsTrackpoint
 import de.westnordost.streetcomplete.ApplicationConstants
 import de.westnordost.streetcomplete.data.download.ConnectionException
 import de.westnordost.streetcomplete.data.user.AuthorizationException
-import java.time.Instant
-import java.time.ZoneOffset
-import java.time.format.DateTimeFormatter
+import kotlinx.datetime.Instant
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toJavaInstant
+import kotlinx.datetime.toLocalDateTime

 class TracksApiImpl(osm: OsmConnection) : TracksApi {
     private val api: GpsTracesApi = GpsTracesApi(osm)
@@ -22,10 +24,7 @@ class TracksApiImpl(osm: OsmConnection) : TracksApi {
     override fun create(trackpoints: List<Trackpoint>, noteText: String?): Long = wrapExceptions {
         // Filename is just the start of the track
         // https://stackoverflow.com/a/49862573/7718197
-        val name = DateTimeFormatter
-            .ofPattern("yyyy_MM_dd'T'HH_mm_ss.SSSSSS'Z'")
-            .withZone(ZoneOffset.UTC)
-            .format(Instant.ofEpochMilli(trackpoints[0].time)) + ".gpx"
+        val name = Instant.fromEpochMilliseconds(trackpoints[0].time).toLocalDateTime(TimeZone.UTC).toTrackFilename()
         val visibility = GpsTraceDetails.Visibility.IDENTIFIABLE
         val description = noteText ?: "Uploaded via ${ApplicationConstants.USER_AGENT}"
         val tags = listOf(ApplicationConstants.NAME.lowercase())
@@ -34,7 +33,7 @@ class TracksApiImpl(osm: OsmConnection) : TracksApi {
         val history = trackpoints.mapIndexed { idx, it ->
             GpsTrackpoint(
                 OsmLatLon(it.position.latitude, it.position.longitude),
-                Instant.ofEpochMilli(it.time),
+                Instant.fromEpochMilliseconds(it.time).toJavaInstant(),
                 idx == 0,
                 it.accuracy,
                 it.elevation
@@ -60,3 +59,9 @@ private inline fun <T> wrapExceptions(block: () -> T): T =
         // request timeout is a temporary connection error
         throw if (e.errorCode == 408) ConnectionException(e.message, e) else e
     }
+
+private fun LocalDateTime.toTrackFilename(): String {
+    fun Int.f(len: Int): String = toString().padStart(len, '0')
+    return ("${year.f(4)}_${monthNumber.f(2)}_${dayOfMonth.f(2)}"
+        + "T${hour.f(2)}_${minute.f(2)}_${second.f(2)}.${nanosecond.f(6)}Z.gpx")
+}
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
index 52d8696f214..384e36c8048 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
@@ -8,9 +8,10 @@ import de.westnordost.streetcomplete.Prefs
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.user.UserLoginStatusSource
 import de.westnordost.streetcomplete.util.ktx.getIds
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
+import de.westnordost.streetcomplete.util.ktx.systemTimeNow
 import de.westnordost.streetcomplete.util.ktx.toLocalDate
-import java.time.Instant
-import java.time.LocalDate
+import kotlinx.datetime.Instant
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.FutureTask

@@ -129,9 +130,9 @@ class StatisticsController(
     }

     private fun updateDaysActive() {
-        val today = LocalDate.now()
-        val lastUpdateDate = Instant.ofEpochMilli(lastUpdate).toLocalDate()
-        lastUpdate = Instant.now().toEpochMilli()
+        val today = systemTimeNow().toLocalDate()
+        val lastUpdateDate = Instant.fromEpochMilliseconds(lastUpdate).toLocalDate()
+        lastUpdate = nowAsEpochMilliseconds()
         if (today > lastUpdateDate) {
             daysActive++
             listeners.forEach { it.onUpdatedDaysActive() }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt
index cd3900b84ad..406238cc70a 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt
@@ -1,7 +1,7 @@
 package de.westnordost.streetcomplete.data.user.statistics

+import kotlinx.datetime.Instant
 import org.json.JSONObject
-import java.time.OffsetDateTime

 class StatisticsParser(private val typeAliases: List<Pair<String, String>>) {
     fun parse(json: String): Statistics {
@@ -27,8 +27,8 @@ class StatisticsParser(private val typeAliases: List<Pair<String, String>>) {
         val rank = obj.getInt("rank")
         val daysActive = obj.getInt("daysActive")
         val isAnalyzing = obj.getBoolean("isAnalyzing")
-        val lastUpdate = OffsetDateTime.parse(obj.getString("lastUpdate")).toInstant()
-        return Statistics(typesStatistics, countriesStatistics, rank, daysActive, lastUpdate.toEpochMilli(), isAnalyzing)
+        val lastUpdate = Instant.parse(obj.getString("lastUpdate"))
+        return Statistics(typesStatistics, countriesStatistics, rank, daysActive, lastUpdate.toEpochMilliseconds(), isAnalyzing)
     }

     private fun mergeTypeAliases(map: MutableMap<String, Int>) {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/ResurveyUtils.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/ResurveyUtils.kt
index 5199a4ba42d..c0db1317480 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/ResurveyUtils.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/ResurveyUtils.kt
@@ -1,8 +1,8 @@
 package de.westnordost.streetcomplete.osm

-import java.time.DateTimeException
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
+import de.westnordost.streetcomplete.util.ktx.systemTimeNow
+import de.westnordost.streetcomplete.util.ktx.toLocalDate
+import kotlinx.datetime.LocalDate

 /** Returns all the known keys used for recording the date at which the tag with the given key
  *  should be checked again. */
@@ -20,8 +20,10 @@ val LAST_CHECK_DATE_KEYS = listOf(
     "survey_date"
 )

-fun LocalDate.toCheckDateString(): String =
-    DateTimeFormatter.ISO_LOCAL_DATE.format(this)
+@Suppress("NOTHING_TO_INLINE")
+inline fun LocalDate.toCheckDateString(): String = this.toString()
+
+fun nowAsCheckDateString(): String = systemTimeNow().toLocalDate().toCheckDateString()

 fun String.toCheckDate(): LocalDate? {
     val groups = OSM_CHECK_DATE_REGEX.matchEntire(this)?.groupValues ?: return null
@@ -30,8 +32,8 @@ fun String.toCheckDate(): LocalDate? {
     val day = groups[3].toIntOrNull() ?: 1

     return try {
-        LocalDate.of(year, month, day)
-    } catch (e: DateTimeException) {
+        LocalDate(year, month, day)
+    } catch (e: IllegalArgumentException) {
         null
     }
 }
@@ -53,7 +55,7 @@ fun Tags.updateWithCheckDate(key: String, value: String) {
 /** Set/update solely the check date to today for the given key, this also removes other less
  *  preferred check date keys. */
 fun Tags.updateCheckDateForKey(key: String) {
-    setCheckDateForKey(key, LocalDate.now())
+    setCheckDateForKey(key, systemTimeNow().toLocalDate())
 }

 fun Tags.setCheckDateForKey(key: String, date: LocalDate) {
@@ -74,7 +76,7 @@ fun Tags.removeCheckDatesForKey(key: String) {
  *  preferred check date keys for the entire item. */
 fun Tags.updateCheckDate() {
     removeCheckDates()
-    set(SURVEY_MARK_KEY, LocalDate.now().toCheckDateString())
+    set(SURVEY_MARK_KEY, nowAsCheckDateString())
 }

 /** Return whether any check dates are set */
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/CompletedConstructionAnswer.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/CompletedConstructionAnswer.kt
index f158327713e..9775f50b7f3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/CompletedConstructionAnswer.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/CompletedConstructionAnswer.kt
@@ -1,6 +1,6 @@
 package de.westnordost.streetcomplete.quests.construction

-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 sealed interface CompletedConstructionAnswer
 data class StateAnswer(val value: Boolean) : CompletedConstructionAnswer
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedConstructionForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedConstructionForm.kt
index 4ea6981576f..d65335b0908 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedConstructionForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/construction/MarkCompletedConstructionForm.kt
@@ -4,8 +4,12 @@ import android.app.DatePickerDialog
 import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm
 import de.westnordost.streetcomplete.quests.AnswerItem
+import de.westnordost.streetcomplete.util.ktx.systemTimeNow
 import de.westnordost.streetcomplete.util.ktx.toInstant
-import java.time.LocalDate
+import de.westnordost.streetcomplete.util.ktx.toLocalDate
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.plus

 class MarkCompletedConstructionForm : AbstractOsmQuestForm<CompletedConstructionAnswer>() {

@@ -19,12 +23,12 @@ class MarkCompletedConstructionForm : AbstractOsmQuestForm<CompletedConstruction
     )

     private fun setFinishDate() {
-        val tomorrow = LocalDate.now().plusDays(1)
+        val tomorrow = systemTimeNow().toLocalDate().plus(1, DateTimeUnit.DAY)
         val dpd = DatePickerDialog(requireContext(), { _, year, month, day ->
-            applyAnswer(OpeningDateAnswer(LocalDate.of(year, month + 1, day)))
-        }, tomorrow.year, tomorrow.monthValue - 1, tomorrow.dayOfMonth)
+            applyAnswer(OpeningDateAnswer(LocalDate(year, month + 1, day)))
+        }, tomorrow.year, tomorrow.monthNumber - 1, tomorrow.dayOfMonth)
         dpd.setTitle(resources.getString(R.string.quest_construction_completion_date_title))
-        dpd.datePicker.minDate = tomorrow.toInstant().toEpochMilli()
+        dpd.datePicker.minDate = tomorrow.toInstant().toEpochMilliseconds()
         dpd.show()
     }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt
index d4b46d22f75..6b7ae8c2897 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt
@@ -30,6 +30,7 @@ import de.westnordost.streetcomplete.quests.AbstractQuestForm
 import de.westnordost.streetcomplete.quests.AnswerItem
 import de.westnordost.streetcomplete.util.ktx.createBitmap
 import de.westnordost.streetcomplete.util.ktx.nonBlankTextOrNull
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
 import de.westnordost.streetcomplete.view.CircularOutlineProvider
 import de.westnordost.streetcomplete.view.ListAdapter
@@ -40,7 +41,6 @@ import kotlinx.coroutines.withContext
 import org.koin.android.ext.android.inject
 import org.koin.core.qualifier.named
 import java.io.File
-import java.time.Instant

 class NoteDiscussionForm : AbstractQuestForm() {

@@ -158,7 +158,7 @@ class NoteDiscussionForm : AbstractQuestForm() {
         }

         override fun onBind(with: NoteComment) {
-            val dateDescription = DateUtils.getRelativeTimeSpanString(with.timestamp, Instant.now().toEpochMilli(), MINUTE_IN_MILLIS)
+            val dateDescription = DateUtils.getRelativeTimeSpanString(with.timestamp, nowAsEpochMilliseconds(), MINUTE_IN_MILLIS)
             val userName = if (with.user != null) with.user.displayName else getString(R.string.quest_noteDiscussion_anonymous)

             val commentActionResourceId = with.action.actionResourceId
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/TakePhoto.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/TakePhoto.kt
index 726712d0116..7dcb3932b30 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/TakePhoto.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/note_discussion/TakePhoto.kt
@@ -14,6 +14,7 @@ import de.westnordost.streetcomplete.R
 import de.westnordost.streetcomplete.util.ActivityForResultLauncher
 import de.westnordost.streetcomplete.util.decodeScaledBitmapAndNormalize
 import de.westnordost.streetcomplete.util.ktx.hasCameraPermission
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
@@ -66,7 +67,7 @@ class TakePhoto(

     private fun createImageFile(context: Context): File {
         val directory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
-        val imageFileName = "photo_" + System.currentTimeMillis() + ".jpg"
+        val imageFileName = "photo_" + nowAsEpochMilliseconds() + ".jpg"
         val file = File(directory, imageFileName)
         if (!file.createNewFile()) throw IOException("Photo file with exactly the same name already exists")
         return file
diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
index a3b4a7a9e6b..47dadd87881 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSigned.kt
@@ -15,9 +15,9 @@ import de.westnordost.streetcomplete.osm.setCheckDateForKey
 import de.westnordost.streetcomplete.osm.toCheckDate
 import de.westnordost.streetcomplete.osm.updateCheckDateForKey
 import de.westnordost.streetcomplete.quests.YesNoQuestForm
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
 import java.util.concurrent.FutureTask

 class CheckOpeningHoursSigned(
@@ -75,9 +75,9 @@ class CheckOpeningHoursSigned(
                 .any { tags[it]?.toCheckDate() != null }

             if (!hasCheckDate) {
-                tags.setCheckDateForKey("opening_hours", LocalDateTime.ofInstant(
-                    Instant.ofEpochMilli(timestampEdited), ZoneId.systemDefault()
-                ).toLocalDate())
+                tags.setCheckDateForKey("opening_hours", Instant.fromEpochMilliseconds(timestampEdited)
+                    .toLocalDateTime(TimeZone.currentSystemDefault())
+                .date)
             }
         } else {
             tags["opening_hours:signed"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryAdapter.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryAdapter.kt
index 568966a255e..4f71dab43a0 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryAdapter.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryAdapter.kt
@@ -14,8 +14,8 @@ import de.westnordost.streetcomplete.data.edithistory.overlayIcon
 import de.westnordost.streetcomplete.databinding.RowEditItemBinding
 import de.westnordost.streetcomplete.databinding.RowEditSyncedBinding
 import de.westnordost.streetcomplete.util.ktx.findNext
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.ktx.toast
-import java.lang.System.currentTimeMillis
 import java.text.DateFormat
 import java.util.Collections
 import kotlin.collections.ArrayList
@@ -191,7 +191,7 @@ class EditHistoryAdapter(
 }

 private fun Edit.formatSameDayTime() = DateUtils.formatSameDayTime(
-    createdTimestamp, currentTimeMillis(), DateFormat.SHORT, DateFormat.SHORT
+    createdTimestamp, nowAsEpochMilliseconds(), DateFormat.SHORT, DateFormat.SHORT
 )

 private fun Edit.formatDate() = DateFormat.getDateInstance(DateFormat.SHORT).format(createdTimestamp)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/UndoDialog.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/UndoDialog.kt
index c9ea59d62a4..a332b36840f 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/UndoDialog.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/edithistory/UndoDialog.kt
@@ -35,6 +35,7 @@ import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestHidden
 import de.westnordost.streetcomplete.data.quest.QuestType
 import de.westnordost.streetcomplete.databinding.DialogUndoBinding
 import de.westnordost.streetcomplete.quests.getHtmlQuestTitle
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.view.CharSequenceText
 import de.westnordost.streetcomplete.view.ResText
 import de.westnordost.streetcomplete.view.Text
@@ -66,7 +67,7 @@ class UndoDialog(
         val overlayResId = edit.overlayIcon
         if (overlayResId != 0) binding.overlayIcon.setImageResource(overlayResId)
         binding.createdTimeText.text =
-            DateUtils.getRelativeTimeSpanString(edit.createdTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS)
+            DateUtils.getRelativeTimeSpanString(edit.createdTimestamp, nowAsEpochMilliseconds(), DateUtils.MINUTE_IN_MILLIS)
         binding.descriptionContainer.addView(edit.descriptionView)

         setTitle(R.string.undo_confirm_title2)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/Compass.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/Compass.kt
index 3864a8dd390..e316a8f2ef6 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/Compass.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/Compass.kt
@@ -12,6 +12,7 @@ import android.view.Display
 import android.view.Surface
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import java.lang.Math.toRadians
 import kotlin.math.PI
 import kotlin.math.abs
@@ -158,7 +159,7 @@ class Compass(
             location.latitude.toFloat(),
             location.longitude.toFloat(),
             location.altitude.toFloat(),
-            System.currentTimeMillis()
+            nowAsEpochMilliseconds()
         )
         declination = toRadians(geomagneticField.declination.toDouble()).toFloat()
     }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/CameraManager.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/CameraManager.kt
index 2d36bd75643..99468c0c31b 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/CameraManager.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/tangram/CameraManager.kt
@@ -18,6 +18,7 @@ import androidx.core.animation.addListener
 import com.mapzen.tangram.CameraUpdateFactory
 import com.mapzen.tangram.MapController
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.ktx.runImmediate
 import kotlin.math.PI

@@ -155,7 +156,7 @@ class CameraManager(private val c: MapController, private val contentResolver: C
             unassignAnimation(animator)
         })

-        val endTime = System.currentTimeMillis() + duration
+        val endTime = nowAsEpochMilliseconds() + duration
         if (lastAnimatorEndTime < endTime) {
             lastAnimator?.removeAllUpdateListeners()
             lastAnimator = animator
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/PhysicsWorldController.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/PhysicsWorldController.kt
index 32ef19bf80d..9c9b41c639d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/PhysicsWorldController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/PhysicsWorldController.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.screens.user.statistics

 import android.os.Handler
 import android.os.HandlerThread
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.withContext
 import org.jbox2d.collision.shapes.Shape
@@ -64,9 +65,9 @@ class PhysicsWorldController(gravity: Vec2) {
     }

     private fun loop() {
-        val startTime = System.currentTimeMillis()
+        val startTime = nowAsEpochMilliseconds()
         world.step(DELAY / 1000f, 6, 2)
-        val executionTime = System.currentTimeMillis() - startTime
+        val executionTime = nowAsEpochMilliseconds() - startTime
         listener?.onWorldStep()
         if (isRunning) {
             handler.postDelayed(this::loop, max(0, DELAY - executionTime))
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt b/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt
index 3f1bfdb0c65..9259c7a80f6 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/DateTime.kt
@@ -1,17 +1,18 @@
 package de.westnordost.streetcomplete.util

+import de.westnordost.streetcomplete.util.ktx.systemTimeNow
 import java.text.DateFormat
-import java.time.LocalDate
-import java.time.LocalDateTime
-import java.time.LocalTime
-import java.time.ZoneId
+import de.westnordost.streetcomplete.util.ktx.toLocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toInstant
 import java.util.Locale

 fun timeOfDayToString(locale: Locale, minutes: Int): String {
     val seconds = (minutes % (24 * 60)) * 60L
-    val todayAt = LocalDateTime.of(LocalDate.now(), LocalTime.ofSecondOfDay(seconds))
-        .atZone(ZoneId.systemDefault())
-        .toInstant()
-        .toEpochMilli()
+    val todayAt = LocalDateTime(systemTimeNow().toLocalDate(), LocalTime.fromSecondOfDay(seconds.toInt()))
+        .toInstant(TimeZone.currentSystemDefault())
+        .toEpochMilliseconds()
     return DateFormat.getTimeInstance(DateFormat.SHORT, locale).format(todayAt)
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDate.kt b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDate.kt
index 81919c91533..a464554558d 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDate.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/util/ktx/LocalDate.kt
@@ -1,19 +1,48 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
 package de.westnordost.streetcomplete.util.ktx

-import java.time.Instant
-import java.time.LocalDate
-import java.time.Month
-import java.time.ZoneId
+import kotlinx.datetime.Clock
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.Instant
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.Month
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.atStartOfDayIn
+import kotlinx.datetime.minus
+import kotlinx.datetime.plus
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime

 fun LocalDate.toInstant(): Instant =
-    this.atStartOfDay(ZoneId.systemDefault()).toInstant()
+    this.atStartOfDayIn(TimeZone.currentSystemDefault())

-fun LocalDate.toEpochMilli(): Long = this.toInstant().toEpochMilli()
+fun LocalDate.toEpochMilli(): Long = this.toInstant().toEpochMilliseconds()

 fun Instant.toLocalDate(): LocalDate =
-    this.atZone(ZoneId.systemDefault()).toLocalDate()
+    this.toLocalDateTime(TimeZone.currentSystemDefault()).date

 fun isApril1st(): Boolean {
-    val now = LocalDate.now()
+    val now = systemTimeNow().toLocalDate()
     return now.dayOfMonth == 1 && now.month == Month.APRIL
 }
+
+fun nowAsEpochMilliseconds(): Long = systemTimeNow().toEpochMilliseconds()
+
+fun systemTimeNow(): Instant = Clock.System.now()
+
+fun LocalDateTime.Companion.now(): LocalDateTime =
+    systemTimeNow().toLocalDateTime(TimeZone.currentSystemDefault())
+
+/** https://github.com/Kotlin/kotlinx-datetime#date--time-arithmetic */
+fun LocalDateTime.minusInSystemTimeZone(value: Long, unit: DateTimeUnit): LocalDateTime {
+    val tz = TimeZone.currentSystemDefault()
+    return toInstant(tz).minus(value, unit, tz).toLocalDateTime(tz)
+}
+
+/** https://github.com/Kotlin/kotlinx-datetime#date--time-arithmetic */
+fun LocalDateTime.plusInSystemTimeZone(value: Long, unit: DateTimeUnit): LocalDateTime {
+    val tz = TimeZone.currentSystemDefault()
+    return toInstant(tz).plus(value, unit, tz).toLocalDateTime(tz)
+}

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDaoTest.kt
index 293aeef8298..2d59655fa10 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/download/tiles/DownloadedTilesDaoTest.kt
@@ -1,6 +1,7 @@
 package de.westnordost.streetcomplete.data.download.tiles

 import de.westnordost.streetcomplete.data.ApplicationDbTestCase
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -24,7 +25,7 @@ class DownloadedTilesDaoTest : ApplicationDbTestCase() {

     @Test fun putGetOld() {
         dao.put(r(5, 8, 5, 8), "Huhu")
-        val huhus = dao.get(r(5, 8, 5, 8), System.currentTimeMillis() + 1000)
+        val huhus = dao.get(r(5, 8, 5, 8), nowAsEpochMilliseconds() + 1000)
         assertTrue(huhus.isEmpty())
     }

@@ -32,7 +33,7 @@ class DownloadedTilesDaoTest : ApplicationDbTestCase() {
         dao.put(r(0, 0, 1, 3), "Huhu")
         Thread.sleep(2000)
         dao.put(r(1, 3, 5, 5), "Huhu")
-        val huhus = dao.get(r(0, 0, 2, 2), System.currentTimeMillis() - 1000)
+        val huhus = dao.get(r(0, 0, 2, 2), nowAsEpochMilliseconds() - 1000)
         assertTrue(huhus.isEmpty())
     }

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDaoTest.kt
index 4afe5d5cda4..f1afbf2bee5 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/NodeDaoTest.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.data.osm.mapdata

 import de.westnordost.streetcomplete.data.ApplicationDbTestCase
 import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -9,7 +10,6 @@ import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
-import java.lang.System.currentTimeMillis

 class NodeDaoTest : ApplicationDbTestCase() {
     private lateinit var dao: NodeDao
@@ -76,13 +76,13 @@ class NodeDaoTest : ApplicationDbTestCase() {

     @Test fun getUnusedAndOldIds() {
         dao.putAll(listOf(nd(1L), nd(2L), nd(3L)))
-        val unusedIds = dao.getIdsOlderThan(currentTimeMillis() + 10)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10)
         assertTrue(unusedIds.containsExactlyInAnyOrder(listOf(1L, 2L, 3L)))
     }

     @Test fun getUnusedAndOldIdsButAtMostX() {
         dao.putAll(listOf(nd(1L), nd(2L), nd(3L)))
-        val unusedIds = dao.getIdsOlderThan(currentTimeMillis() + 10, 2)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10, 2)
         assertEquals(2, unusedIds.size)
     }

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDaoTest.kt
index 39ca5d00864..f5fe9615d13 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/RelationDaoTest.kt
@@ -5,6 +5,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.ElementType.NODE
 import de.westnordost.streetcomplete.data.osm.mapdata.ElementType.RELATION
 import de.westnordost.streetcomplete.data.osm.mapdata.ElementType.WAY
 import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -148,13 +149,13 @@ class RelationDaoTest : ApplicationDbTestCase() {

     @Test fun getUnusedAndOldIds() {
         dao.putAll(listOf(rel(1L), rel(2L), rel(3L)))
-        val unusedIds = dao.getIdsOlderThan(System.currentTimeMillis() + 10)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10)
         assertTrue(unusedIds.containsExactlyInAnyOrder(listOf(1L, 2L, 3L)))
     }

     @Test fun getUnusedAndOldIdsButAtMostX() {
         dao.putAll(listOf(rel(1L), rel(2L), rel(3L)))
-        val unusedIds = dao.getIdsOlderThan(System.currentTimeMillis() + 10, 2)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10, 2)
         assertEquals(2, unusedIds.size)
     }

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDaoTest.kt
index 1c9d5e53d53..a11cbee0b9d 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/mapdata/WayDaoTest.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.data.osm.mapdata

 import de.westnordost.streetcomplete.data.ApplicationDbTestCase
 import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -103,13 +104,13 @@ class WayDaoTest : ApplicationDbTestCase() {

     @Test fun getUnusedAndOldIds() {
         dao.putAll(listOf(way(1L), way(2L), way(3L)))
-        val unusedIds = dao.getIdsOlderThan(System.currentTimeMillis() + 10)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10)
         assertTrue(unusedIds.containsExactlyInAnyOrder(listOf(1L, 2L, 3L)))
     }

     @Test fun getUnusedAndOldIdsButAtMostX() {
         dao.putAll(listOf(way(1L), way(2L), way(3L)))
-        val unusedIds = dao.getIdsOlderThan(System.currentTimeMillis() + 10, 2)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10, 2)
         assertEquals(2, unusedIds.size)
     }

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDaoTest.kt
index c90014ff21d..2f870edddc0 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osm/osmquests/OsmQuestsHiddenDaoTest.kt
@@ -4,6 +4,7 @@ import de.westnordost.streetcomplete.data.ApplicationDbTestCase
 import de.westnordost.streetcomplete.data.osm.mapdata.ElementType
 import de.westnordost.streetcomplete.data.quest.OsmQuestKey
 import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
@@ -44,7 +45,7 @@ class OsmQuestsHiddenDaoTest : ApplicationDbTestCase() {
         )
         dao.add(keys[0])
         delay(200)
-        val time = System.currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         dao.add(keys[1])
         val result = dao.getNewerThan(time - 100).single()
         assertEquals(keys[1], result.osmQuestKey)
diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/NoteDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/NoteDaoTest.kt
index b020db338cc..f502b843ace 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/NoteDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/NoteDaoTest.kt
@@ -5,6 +5,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
 import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
 import de.westnordost.streetcomplete.data.user.User
 import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -101,13 +102,13 @@ class NoteDaoTest : ApplicationDbTestCase() {

     @Test fun getUnusedAndOldIds() {
         dao.putAll(listOf(createNote(1), createNote(2), createNote(3)))
-        val unusedIds = dao.getIdsOlderThan(System.currentTimeMillis() + 10)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10)
         assertTrue(unusedIds.containsExactlyInAnyOrder(listOf(1L, 2L, 3L)))
     }

     @Test fun getUnusedAndOldIdsButAtMostX() {
         dao.putAll(listOf(createNote(1), createNote(2), createNote(3)))
-        val unusedIds = dao.getIdsOlderThan(System.currentTimeMillis() + 10, 2)
+        val unusedIds = dao.getIdsOlderThan(nowAsEpochMilliseconds() + 10, 2)
         assertEquals(2, unusedIds.size)
     }

diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDaoTest.kt
index 06b99200401..18f5724deba 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/osmnotes/notequests/NoteQuestsHiddenDaoTest.kt
@@ -2,6 +2,7 @@ package de.westnordost.streetcomplete.data.osmnotes.notequests

 import de.westnordost.streetcomplete.data.ApplicationDbTestCase
 import de.westnordost.streetcomplete.util.ktx.containsExactlyInAnyOrder
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
@@ -42,7 +43,7 @@ class NoteQuestsHiddenDaoTest : ApplicationDbTestCase() {
     @Test fun getNewerThan() = runBlocking {
         dao.add(1L)
         delay(200)
-        val time = System.currentTimeMillis()
+        val time = nowAsEpochMilliseconds()
         dao.add(2L)
         val result = dao.getNewerThan(time - 100).single()
         assertEquals(2L, result.noteId)
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/ElementFiltersTestUtils.kt b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/ElementFiltersTestUtils.kt
index c23e1372d4f..d5bfee4a481 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/ElementFiltersTestUtils.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/ElementFiltersTestUtils.kt
@@ -3,12 +3,15 @@ package de.westnordost.streetcomplete.data.elementfilter
 import de.westnordost.streetcomplete.data.elementfilter.filters.ElementFilter
 import de.westnordost.streetcomplete.testutils.node
 import de.westnordost.streetcomplete.util.ktx.toEpochMilli
-import java.time.LocalDate
-import java.time.LocalDateTime
+import de.westnordost.streetcomplete.util.ktx.minusInSystemTimeZone
+import de.westnordost.streetcomplete.util.ktx.now
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime

 /** Returns the date x days in the past */
 fun dateDaysAgo(daysAgo: Float): LocalDate =
-    LocalDateTime.now().minusHours((daysAgo * 24).toLong()).toLocalDate()
+    LocalDateTime.now().minusInSystemTimeZone((daysAgo * 24).toLong(), DateTimeUnit.HOUR).date

 fun ElementFilter.matches(tags: Map<String, String>, date: LocalDate? = null): Boolean =
     matches(node(tags = tags, timestamp = date?.toEpochMilli()))
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilterOverpassKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilterOverpassKtTest.kt
index 7d4bc8501f9..523f5dd7a61 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilterOverpassKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/ElementFilterOverpassKtTest.kt
@@ -4,11 +4,11 @@ import de.westnordost.streetcomplete.data.elementfilter.dateDaysAgo
 import de.westnordost.streetcomplete.osm.toCheckDateString
 import org.junit.Assert.assertEquals
 import org.junit.Test
-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 class ElementFilterOverpassKtTest {

-    private val date2000_11_11 = FixedDate(LocalDate.of(2000, 11, 11))
+    private val date2000_11_11 = FixedDate(LocalDate(2000, 11, 11))

     @Test fun tagOlderThan() {
         val date = dateDaysAgo(100f).toCheckDateString()
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterOrEqualThanTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterOrEqualThanTest.kt
index 9a5444f25ba..bcccf97b342 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterOrEqualThanTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterOrEqualThanTest.kt
@@ -5,10 +5,10 @@ import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 class HasDateTagGreaterOrEqualThanTest {
-    private val date = LocalDate.of(2000, 11, 11)
+    private val date = LocalDate(2000, 11, 11)
     private val c = HasDateTagGreaterOrEqualThan("check_date", FixedDate(date))

     @Test fun matches() {
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterThanTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterThanTest.kt
index dd5430771be..e2e4e4ed07d 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterThanTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagGreaterThanTest.kt
@@ -5,10 +5,10 @@ import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 class HasDateTagGreaterThanTest {
-    private val date = LocalDate.of(2000, 11, 11)
+    private val date = LocalDate(2000, 11, 11)
     private val c = HasDateTagGreaterThan("check_date", FixedDate(date))

     @Test fun matches() {
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessOrEqualThanTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessOrEqualThanTest.kt
index 4d070e92895..e7ecc120ec8 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessOrEqualThanTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessOrEqualThanTest.kt
@@ -5,10 +5,10 @@ import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 class HasDateTagLessOrEqualThanTest {
-    private val date = LocalDate.of(2000, 11, 11)
+    private val date = LocalDate(2000, 11, 11)
     private val c = HasDateTagLessOrEqualThan("check_date", FixedDate(date))

     @Test fun matches() {
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessThanTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessThanTest.kt
index de60795e46b..15497d29cd6 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessThanTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/elementfilter/filters/HasDateTagLessThanTest.kt
@@ -5,10 +5,10 @@ import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 class HasDateTagLessThanTest {
-    private val date = LocalDate.of(2000, 11, 11)
+    private val date = LocalDate(2000, 11, 11)
     private val c = HasDateTagLessThan("check_date", FixedDate(date))

     @Test fun matches() {
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/meta/ResurveyUtilsTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/meta/ResurveyUtilsTest.kt
index 32afc7e2234..2aaca3c6d89 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/meta/ResurveyUtilsTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/meta/ResurveyUtilsTest.kt
@@ -5,6 +5,7 @@ import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDe
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
 import de.westnordost.streetcomplete.osm.Tags
 import de.westnordost.streetcomplete.osm.hasCheckDateForKey
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.osm.removeCheckDates
 import de.westnordost.streetcomplete.osm.toCheckDate
 import de.westnordost.streetcomplete.osm.toCheckDateString
@@ -16,15 +17,15 @@ import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate
+import kotlinx.datetime.LocalDate

 class ResurveyUtilsTest {
     @Test fun toCheckDateString() {
-        assertEquals("2007-12-08", LocalDate.of(2007, 12, 8).toCheckDateString())
+        assertEquals("2007-12-08", LocalDate(2007, 12, 8).toCheckDateString())
     }

     @Test fun fromCheckDateString() {
-        assertEquals(LocalDate.of(2007, 12, 8), "2007-12-08".toCheckDate())
+        assertEquals(LocalDate(2007, 12, 8), "2007-12-08".toCheckDate())
     }

     @Test fun `updateWithCheckDate adds new tag`() {
@@ -68,7 +69,7 @@ class ResurveyUtilsTest {

         assertEquals(setOf(
             StringMapEntryModify("key", "value", "value"),
-            StringMapEntryAdd("check_date:key", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:key", nowAsCheckDateString())
         ), changes)
     }

@@ -79,7 +80,7 @@ class ResurveyUtilsTest {

         assertEquals(setOf(
             StringMapEntryModify("key", "value", "value"),
-            StringMapEntryModify("check_date:key", "2000-11-11", LocalDate.now().toCheckDateString())
+            StringMapEntryModify("check_date:key", "2000-11-11", nowAsCheckDateString())
         ), changes)
     }

@@ -98,7 +99,7 @@ class ResurveyUtilsTest {

         assertTrue(changes.containsExactlyInAnyOrder(listOf(
             StringMapEntryModify("key", "old value", "new value"),
-            StringMapEntryModify("check_date:key", "2000-11-02", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:key", "2000-11-02", nowAsCheckDateString()),
             StringMapEntryDelete("key:check_date", "2000-11-01"),
             StringMapEntryDelete("key:lastcheck", "2000-11-03"),
             StringMapEntryDelete("lastcheck:key", "2000-11-04"),
@@ -122,7 +123,7 @@ class ResurveyUtilsTest {
         assertTrue(changes.containsExactlyInAnyOrder(listOf(
             StringMapEntryAdd("key", "value"),
             StringMapEntryDelete("key:check_date", "2000-11-01"),
-            StringMapEntryModify("check_date:key", "2000-11-02", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:key", "2000-11-02", nowAsCheckDateString()),
             StringMapEntryDelete("key:lastcheck", "2000-11-03"),
             StringMapEntryDelete("lastcheck:key", "2000-11-04"),
             StringMapEntryDelete("key:last_checked", "2000-11-05"),
@@ -145,7 +146,7 @@ class ResurveyUtilsTest {

         assertTrue(changes.containsExactlyInAnyOrder(listOf(
             StringMapEntryModify("key", "value", "value"),
-            StringMapEntryModify("check_date:key", "2000-11-01", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:key", "2000-11-01", nowAsCheckDateString()),
             StringMapEntryDelete("key:check_date", "2000-11-02"),
             StringMapEntryDelete("key:lastcheck", "2000-11-03"),
             StringMapEntryDelete("lastcheck:key", "2000-11-04"),
@@ -160,7 +161,7 @@ class ResurveyUtilsTest {
         val changes = builder.create().changes

         assertEquals(setOf(
-            StringMapEntryAdd("check_date:key", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:key", nowAsCheckDateString())
         ), changes)
     }

@@ -170,7 +171,7 @@ class ResurveyUtilsTest {
         val changes = builder.create().changes

         assertEquals(setOf(
-            StringMapEntryModify("check_date:key", "2000-11-11", LocalDate.now().toCheckDateString())
+            StringMapEntryModify("check_date:key", "2000-11-11", nowAsCheckDateString())
         ), changes)
     }

@@ -187,7 +188,7 @@ class ResurveyUtilsTest {
         val changes = builder.create().changes.toSet()

         assertTrue(changes.containsExactlyInAnyOrder(listOf(
-            StringMapEntryModify("check_date:key", "2000-11-01", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:key", "2000-11-01", nowAsCheckDateString()),
             StringMapEntryDelete("key:check_date", "2000-11-02"),
             StringMapEntryDelete("key:lastcheck", "2000-11-03"),
             StringMapEntryDelete("lastcheck:key", "2000-11-04"),
@@ -202,7 +203,7 @@ class ResurveyUtilsTest {
         val changes = builder.create().changes

         assertEquals(setOf(
-            StringMapEntryAdd("check_date", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date", nowAsCheckDateString())
         ), changes)
     }

@@ -212,7 +213,7 @@ class ResurveyUtilsTest {
         val changes = builder.create().changes

         assertEquals(setOf(
-            StringMapEntryModify("check_date", "2000-11-11", LocalDate.now().toCheckDateString())
+            StringMapEntryModify("check_date", "2000-11-11", nowAsCheckDateString())
         ), changes)
     }

@@ -226,7 +227,7 @@ class ResurveyUtilsTest {
         val changes = builder.create().changes.toSet()

         assertTrue(changes.containsExactlyInAnyOrder(listOf(
-            StringMapEntryModify("check_date", "2000-11-01", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date", "2000-11-01", nowAsCheckDateString()),
             StringMapEntryDelete("lastcheck", "2000-11-02"),
             StringMapEntryDelete("last_checked", "2000-11-03"),
         )))
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt
index ef4d6263642..0cfe9c54bd2 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt
@@ -1,8 +1,8 @@
 package de.westnordost.streetcomplete.data.user.statistics

+import kotlinx.datetime.Instant
 import org.junit.Assert.assertEquals
 import org.junit.Test
-import java.time.OffsetDateTime

 class StatisticsParserTest {

@@ -24,7 +24,7 @@ class StatisticsParserTest {
             ),
             2345,
             78,
-            OffsetDateTime.parse("2007-12-03T10:15:30+01:00").toInstant().toEpochMilli(),
+            Instant.parse("2007-12-03T10:15:30+01:00").toEpochMilliseconds(),
             false
         ),
         StatisticsParser(listOf("TestQuestTypeCAlias" to "TestQuestTypeC")).parse("""
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/lit/LitStatusKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/lit/LitStatusKtTest.kt
index 1b4e7b81b8e..02441aefe33 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/lit/LitStatusKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/lit/LitStatusKtTest.kt
@@ -5,10 +5,9 @@ import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
 import de.westnordost.streetcomplete.osm.lit.LitStatus.*
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import org.assertj.core.api.Assertions
 import org.junit.Test
-import java.time.LocalDate

 class LitStatusKtTest {

@@ -26,7 +25,7 @@ class LitStatusKtTest {
     }

     @Test fun `apply updates check date`() {
-        val today = LocalDate.now().toCheckDateString()
+        val today = nowAsCheckDateString()
         verifyAnswer(
             mapOf("lit" to "yes"),
             YES,
@@ -65,12 +64,12 @@ class LitStatusKtTest {
         verifyAnswer(
             mapOf("lit" to "limited"),
             YES,
-            arrayOf(StringMapEntryAdd("check_date:lit", LocalDate.now().toCheckDateString()))
+            arrayOf(StringMapEntryAdd("check_date:lit", nowAsCheckDateString()))
         )
         verifyAnswer(
             mapOf("lit" to "22:00-05:00"),
             YES,
-            arrayOf(StringMapEntryAdd("check_date:lit", LocalDate.now().toCheckDateString()))
+            arrayOf(StringMapEntryAdd("check_date:lit", nowAsCheckDateString()))
         )
     }

diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt
index 9de0ce63a62..dd2eb346626 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkKtTest.kt
@@ -5,11 +5,10 @@ import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import org.assertj.core.api.Assertions
 import org.junit.Assert.assertEquals
 import org.junit.Test
-import java.time.LocalDate

 class SidewalkKtTest {
     @Test fun `apply simple values`() {
@@ -106,7 +105,7 @@ class SidewalkKtTest {
             LeftAndRightSidewalk(Sidewalk.YES, Sidewalk.YES),
             arrayOf(
                 StringMapEntryModify("sidewalk", "both", "both"),
-                StringMapEntryAdd("check_date:sidewalk", LocalDate.now().toCheckDateString())
+                StringMapEntryAdd("check_date:sidewalk", nowAsCheckDateString())
             )
         )
         verifyAnswer(
@@ -115,7 +114,7 @@ class SidewalkKtTest {
             arrayOf(
                 StringMapEntryModify("sidewalk:left", "separate", "separate"),
                 StringMapEntryModify("sidewalk:right", "no", "no"),
-                StringMapEntryAdd("check_date:sidewalk", LocalDate.now().toCheckDateString())
+                StringMapEntryAdd("check_date:sidewalk", nowAsCheckDateString())
             )
         )
     }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt
index bfb7986ca11..0c9437db9fa 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/street_parking/StreetParkingKtTest.kt
@@ -5,11 +5,10 @@ import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import org.assertj.core.api.Assertions
 import org.junit.Assert.assertEquals
 import org.junit.Test
-import java.time.LocalDate

 class StreetParkingTest {

@@ -112,7 +111,7 @@ class StreetParkingTest {
             LeftAndRightStreetParking(NoStreetParking, NoStreetParking),
             arrayOf(
                 StringMapEntryModify("parking:lane:both", "no", "no"),
-                StringMapEntryAdd("check_date:parking:lane", LocalDate.now().toCheckDateString())
+                StringMapEntryAdd("check_date:parking:lane", nowAsCheckDateString())
             )
         )
         verifyAnswer(
@@ -130,7 +129,7 @@ class StreetParkingTest {
                 StringMapEntryModify("parking:lane:both", "parallel", "parallel"),
                 StringMapEntryModify("parking:lane:left:parallel", "half_on_kerb", "half_on_kerb"),
                 StringMapEntryModify("parking:lane:right:parallel", "on_kerb", "on_kerb"),
-                StringMapEntryAdd("check_date:parking:lane", LocalDate.now().toCheckDateString())
+                StringMapEntryAdd("check_date:parking:lane", nowAsCheckDateString())
             )
         )
     }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileTypeTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileTypeTest.kt
index 0fc6b9fa860..ccfdfde5c4f 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileTypeTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/barrier_type/AddStileTypeTest.kt
@@ -3,10 +3,9 @@ package de.westnordost.streetcomplete.quests.barrier_type
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import org.junit.Test
-import java.time.LocalDate

 class AddStileTypeTest {
     private val questType = AddStileType()
@@ -30,7 +29,7 @@ class AddStileTypeTest {
                 "stile" to "squeezer",
             ),
             StileType.SQUEEZER,
-            StringMapEntryAdd("check_date", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date", nowAsCheckDateString()),
             StringMapEntryModify("stile", "squeezer", "squeezer"),
         )
     }
@@ -121,7 +120,7 @@ class AddStileTypeTest {
                 "tag_not_in_list_for_removal" to "dummy_value",
             ),
             StileType.STEPOVER_WOODEN,
-            StringMapEntryAdd("check_date", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date", nowAsCheckDateString()),
             StringMapEntryModify("stile", "stepover", "stepover"),
             StringMapEntryModify("material", "wood", "wood"),

diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelterTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelterTest.kt
index 403a57d6f1e..67a2101f255 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelterTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/bus_stop_shelter/AddBusStopShelterTest.kt
@@ -3,10 +3,9 @@ package de.westnordost.streetcomplete.quests.bus_stop_shelter
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import org.junit.Test
-import java.time.LocalDate

 class AddBusStopShelterTest {

@@ -24,7 +23,7 @@ class AddBusStopShelterTest {
             mapOf("shelter" to "yes"),
             BusStopShelterAnswer.SHELTER,
             StringMapEntryModify("shelter", "yes", "yes"),
-            StringMapEntryAdd("check_date:shelter", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:shelter", nowAsCheckDateString())
         )
     }

@@ -40,7 +39,7 @@ class AddBusStopShelterTest {
             mapOf("shelter" to "no"),
             BusStopShelterAnswer.NO_SHELTER,
             StringMapEntryModify("shelter", "no", "no"),
-            StringMapEntryAdd("check_date:shelter", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:shelter", nowAsCheckDateString())
         )
     }

diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingTypeTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingTypeTest.kt
index f05b0a25db0..c7f813f38f5 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingTypeTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/crossing_type/AddCrossingTypeTest.kt
@@ -2,12 +2,11 @@ package de.westnordost.streetcomplete.quests.crossing_type

 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.crossing_type.CrossingType.MARKED
 import de.westnordost.streetcomplete.quests.crossing_type.CrossingType.TRAFFIC_SIGNALS
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import org.junit.Test
-import java.time.LocalDate

 class AddCrossingTypeTest {

@@ -42,19 +41,19 @@ class AddCrossingTypeTest {
         questType.verifyAnswer(
             mapOf("crossing" to "zebra"),
             MARKED,
-            StringMapEntryAdd("check_date:crossing", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:crossing", nowAsCheckDateString())
         )

         questType.verifyAnswer(
             mapOf("crossing" to "marked"),
             MARKED,
-            StringMapEntryAdd("check_date:crossing", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:crossing", nowAsCheckDateString())
         )

         questType.verifyAnswer(
             mapOf("crossing" to "uncontrolled"),
             MARKED,
-            StringMapEntryAdd("check_date:crossing", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:crossing", nowAsCheckDateString())
         )

         questType.verifyAnswer(
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
index dba9b0dc0bc..11d990ea720 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/cycleway/AddCyclewayTest.kt
@@ -21,7 +21,7 @@ import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SIDEWALK_EXPLICIT
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway.SUGGESTION_LANE
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway.TRACK
 import de.westnordost.streetcomplete.osm.cycleway.Cycleway.UNSPECIFIED_LANE
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.TestMapDataWithGeometry
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.mock
@@ -29,6 +29,7 @@ import de.westnordost.streetcomplete.testutils.on
 import de.westnordost.streetcomplete.testutils.p
 import de.westnordost.streetcomplete.testutils.pGeom
 import de.westnordost.streetcomplete.testutils.way
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.math.translate
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -37,8 +38,6 @@ import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.mockito.ArgumentMatchers.anyDouble
-import java.time.Instant
-import java.time.LocalDate
 import java.util.concurrent.FutureTask

 class AddCyclewayTest {
@@ -179,7 +178,7 @@ class AddCyclewayTest {
         val way = way(1L, listOf(1, 2, 3), mapOf(
             "highway" to "primary",
             "cycleway" to "track"
-        ), timestamp = Instant.now().toEpochMilli())
+        ), timestamp = nowAsEpochMilliseconds())
         val mapData = TestMapDataWithGeometry(listOf(way))

         assertEquals(0, questType.getApplicableElements(mapData).toList().size)
@@ -191,7 +190,7 @@ class AddCyclewayTest {
             "highway" to "primary",
             "cycleway" to "track",
             "check_date:cycleway" to "2001-01-01"
-        ), timestamp = Instant.now().toEpochMilli())
+        ), timestamp = nowAsEpochMilliseconds())
         val mapData = TestMapDataWithGeometry(listOf(way))

         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
@@ -203,7 +202,7 @@ class AddCyclewayTest {
             "highway" to "primary",
             "cycleway" to "whatsthis",
             "check_date:cycleway" to "2001-01-01"
-        ), timestamp = Instant.now().toEpochMilli())
+        ), timestamp = nowAsEpochMilliseconds())
         val mapData = TestMapDataWithGeometry(listOf(way))

         assertEquals(0, questType.getApplicableElements(mapData).toList().size)
@@ -598,7 +597,7 @@ class AddCyclewayTest {
             mapOf("cycleway:both" to "track"),
             bothSidesAnswer(TRACK),
             StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryAdd("check_date:cycleway", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:cycleway", nowAsCheckDateString())
         )
     }

@@ -607,7 +606,7 @@ class AddCyclewayTest {
             mapOf("cycleway:both" to "track", "check_date:cycleway" to "2000-11-11"),
             bothSidesAnswer(TRACK),
             StringMapEntryModify("cycleway:both", "track", "track"),
-            StringMapEntryModify("check_date:cycleway", "2000-11-11", LocalDate.now().toCheckDateString())
+            StringMapEntryModify("check_date:cycleway", "2000-11-11", nowAsCheckDateString())
         )
     }

diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterTypeTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterTypeTest.kt
index 083914993e7..ce15f4c9147 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterTypeTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/drinking_water_type/AddDrinkingWaterTypeTest.kt
@@ -3,10 +3,9 @@ package de.westnordost.streetcomplete.quests.drinking_water_type
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.osm.SURVEY_MARK_KEY
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import org.junit.Test
-import java.time.LocalDate

 class AddDrinkingWaterTypeTest {
     private val questType = AddDrinkingWaterType()
@@ -65,7 +64,7 @@ class AddDrinkingWaterTypeTest {
                 "disused:amenity" to "drinking_water",
             ),
             DrinkingWaterType.DISUSED_DRINKING_WATER,
-            StringMapEntryAdd(SURVEY_MARK_KEY, LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd(SURVEY_MARK_KEY, nowAsCheckDateString()),
         )
     }
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt
index f0d72bb358b..330e9912590 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/existence/CheckExistenceTest.kt
@@ -3,11 +3,10 @@ package de.westnordost.streetcomplete.quests.existence
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.mock
 import org.junit.Test
-import java.time.LocalDate

 class CheckExistenceTest {
     private val questType = CheckExistence(mock())
@@ -15,7 +14,7 @@ class CheckExistenceTest {
     @Test fun `apply answer adds check date`() {
         questType.verifyAnswer(
             Unit,
-            StringMapEntryAdd("check_date", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date", nowAsCheckDateString())
         )
     }

@@ -29,7 +28,7 @@ class CheckExistenceTest {
                 "survey_date" to "d"
             ),
             Unit,
-            StringMapEntryModify("check_date", "1", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date", "1", nowAsCheckDateString()),
             StringMapEntryDelete("lastcheck", "a"),
             StringMapEntryDelete("last_checked", "b"),
             StringMapEntryDelete("survey:date", "c"),
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursTest.kt
index 8363d9507f4..b394ee08690 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours/AddOpeningHoursTest.kt
@@ -6,18 +6,17 @@ import ch.poole.openinghoursparser.WeekDay
 import ch.poole.openinghoursparser.WeekDayRange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.osm.opening_hours.parser.OpeningHoursRuleList
 import de.westnordost.streetcomplete.osm.toCheckDate
-import de.westnordost.streetcomplete.osm.toCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.mock
 import de.westnordost.streetcomplete.testutils.node
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import de.westnordost.streetcomplete.util.ktx.toEpochMilli
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.lang.System.currentTimeMillis
-import java.time.LocalDate

 class AddOpeningHoursTest {

@@ -43,7 +42,7 @@ class AddOpeningHoursTest {
             mapOf("opening_hours" to "\"oh\""),
             DescribeOpeningHours("oh"),
             StringMapEntryModify("opening_hours", "\"oh\"", "\"oh\""),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -51,7 +50,7 @@ class AddOpeningHoursTest {
         questType.verifyAnswer(
             NoOpeningHoursSign,
             StringMapEntryAdd("opening_hours:signed", "no"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -60,7 +59,7 @@ class AddOpeningHoursTest {
             mapOf("opening_hours" to "oh"),
             NoOpeningHoursSign,
             StringMapEntryAdd("opening_hours:signed", "no"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -69,7 +68,7 @@ class AddOpeningHoursTest {
             mapOf("opening_hours" to "24/7"),
             NoOpeningHoursSign,
             StringMapEntryAdd("opening_hours:signed", "no"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -93,7 +92,7 @@ class AddOpeningHoursTest {
             mapOf("opening_hours" to "24/7"),
             AlwaysOpen,
             StringMapEntryModify("opening_hours", "24/7", "24/7"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -113,7 +112,7 @@ class AddOpeningHoursTest {
             ),
             AlwaysOpen,
             StringMapEntryModify("opening_hours", "24/7", "24/7"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -187,7 +186,7 @@ class AddOpeningHoursTest {
                 })
             )),
             StringMapEntryModify("opening_hours", "Mo 10:00-12:00", "Mo 10:00-12:00"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString())
         )
     }

@@ -205,34 +204,34 @@ class AddOpeningHoursTest {

     @Test fun `isApplicableTo returns false for known places with recently edited opening hours`() {
         assertFalse(questType.isApplicableTo(
-            node(tags = mapOf("shop" to "sports", "name" to "Atze's Angelladen", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = currentTimeMillis())
+            node(tags = mapOf("shop" to "sports", "name" to "Atze's Angelladen", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = nowAsEpochMilliseconds())
         ))
     }

     @Test fun `isApplicableTo returns true for known places with old opening hours`() {
         val milisecondsFor400Days: Long = 1000L * 60 * 60 * 24 * 400
         assertTrue(questType.isApplicableTo(
-            node(tags = mapOf("shop" to "sports", "name" to "Atze's Angelladen", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = currentTimeMillis() - milisecondsFor400Days)
+            node(tags = mapOf("shop" to "sports", "name" to "Atze's Angelladen", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = nowAsEpochMilliseconds() - milisecondsFor400Days)
         ))
     }

     @Test fun `isApplicableTo returns false for closed shops with old opening hours`() {
         val milisecondsFor400Days: Long = 1000L * 60 * 60 * 24 * 400
         assertFalse(questType.isApplicableTo(
-            node(tags = mapOf("nonexisting:shop" to "sports", "name" to "Atze's Angelladen", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = currentTimeMillis() - milisecondsFor400Days)
+            node(tags = mapOf("nonexisting:shop" to "sports", "name" to "Atze's Angelladen", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = nowAsEpochMilliseconds() - milisecondsFor400Days)
         ))
     }

     @Test fun `isApplicableTo returns true for parks with old opening hours`() {
         val milisecondsFor400Days: Long = 1000L * 60 * 60 * 24 * 400
         assertTrue(questType.isApplicableTo(
-            node(tags = mapOf("leisure" to "park", "name" to "Trolololo", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = currentTimeMillis() - milisecondsFor400Days)
+            node(tags = mapOf("leisure" to "park", "name" to "Trolololo", "opening_hours" to "Mo-Fr 10:00-20:00"), timestamp = nowAsEpochMilliseconds() - milisecondsFor400Days)
         ))
     }

     @Test fun `isApplicableTo returns false for toilets without opening hours`() {
         assertFalse(questType.isApplicableTo(
-            node(tags = mapOf("amenity" to "toilets"), timestamp = currentTimeMillis())
+            node(tags = mapOf("amenity" to "toilets"), timestamp = nowAsEpochMilliseconds())
         ))
     }

diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSignedTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSignedTest.kt
index c09a969085b..6c616118133 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSignedTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/opening_hours_signed/CheckOpeningHoursSignedTest.kt
@@ -3,14 +3,13 @@ package de.westnordost.streetcomplete.quests.opening_hours_signed
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.mock
 import de.westnordost.streetcomplete.testutils.node
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate

 class CheckOpeningHoursSignedTest {
     private val questType = CheckOpeningHoursSigned(mock())
@@ -40,7 +39,7 @@ class CheckOpeningHoursSignedTest {
     @Test fun `is not applicable to place with new check_date`() {
         assertFalse(questType.isApplicableTo(node(tags = mapOf(
             "name" to "XYZ",
-            "check_date:opening_hours" to LocalDate.now().toCheckDateString(),
+            "check_date:opening_hours" to nowAsCheckDateString(),
             "opening_hours:signed" to "no"
         ))))
     }
@@ -117,7 +116,7 @@ class CheckOpeningHoursSignedTest {
             mapOf("opening_hours:signed" to "no"),
             false,
             StringMapEntryModify("opening_hours:signed", "no", "no"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString()),
         )
     }

@@ -129,7 +128,7 @@ class CheckOpeningHoursSignedTest {
             ),
             false,
             StringMapEntryModify("opening_hours:signed", "no", "no"),
-            StringMapEntryModify("check_date:opening_hours", "2020-03-04", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:opening_hours", "2020-03-04", nowAsCheckDateString()),
         )
     }

@@ -141,7 +140,7 @@ class CheckOpeningHoursSignedTest {
             ),
             false,
             StringMapEntryModify("opening_hours:signed", "no", "no"),
-            StringMapEntryAdd("check_date:opening_hours", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date:opening_hours", nowAsCheckDateString()),
         )
     }

@@ -154,7 +153,7 @@ class CheckOpeningHoursSignedTest {
             ),
             false,
             StringMapEntryModify("opening_hours:signed", "no", "no"),
-            StringMapEntryModify("check_date:opening_hours", "2020-03-04", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:opening_hours", "2020-03-04", nowAsCheckDateString()),
         )
     }
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeTest.kt
index 6ed48e60614..558710c4799 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/parking_fee/AddParkingFeeTest.kt
@@ -7,11 +7,10 @@ import ch.poole.openinghoursparser.WeekDayRange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.osm.opening_hours.parser.OpeningHoursRuleList
-import de.westnordost.streetcomplete.osm.toCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import org.junit.Test
-import java.time.LocalDate

 class AddParkingFeeTest {

@@ -78,7 +77,7 @@ class AddParkingFeeTest {
             FeeAndMaxStay(HasFeeExceptAtHours(openingHours)),
             StringMapEntryModify("fee", "yes", "yes"),
             StringMapEntryAdd("fee:conditional", "no @ ($openingHoursString)"),
-            StringMapEntryAdd("check_date:fee", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:fee", nowAsCheckDateString())
         )
     }

@@ -123,10 +122,10 @@ class AddParkingFeeTest {
             mapOf("fee" to "no", "maxstay" to "no", "maxstay:conditional" to "1 hour @ ($openingHoursString)"),
             FeeAndMaxStay(HasNoFee, MaxstayAtHours(MaxstayDuration(1.0, Maxstay.Unit.HOURS), openingHours)),
             StringMapEntryModify("fee", "no", "no"),
-            StringMapEntryAdd("check_date:fee", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date:fee", nowAsCheckDateString()),
             StringMapEntryModify("maxstay:conditional", "1 hour @ ($openingHoursString)", "1 hour @ ($openingHoursString)"),
             StringMapEntryModify("maxstay", "no", "no"),
-            StringMapEntryAdd("check_date:maxstay", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:maxstay", nowAsCheckDateString())
         )
     }
 }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterialsTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterialsTest.kt
index 3e32eeea348..fddbfa54eea 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterialsTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/recycling_material/AddRecyclingContainerMaterialsTest.kt
@@ -3,7 +3,7 @@ package de.westnordost.streetcomplete.quests.recycling_material
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.TestMapDataWithGeometry
 import de.westnordost.streetcomplete.quests.recycling_material.RecyclingMaterial.CLOTHES
 import de.westnordost.streetcomplete.quests.recycling_material.RecyclingMaterial.PAPER
@@ -14,10 +14,9 @@ import de.westnordost.streetcomplete.quests.recycling_material.RecyclingMaterial
 import de.westnordost.streetcomplete.quests.recycling_material.RecyclingMaterial.SHOES
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.node
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds
 import org.junit.Assert.assertEquals
 import org.junit.Test
-import java.time.Instant
-import java.time.LocalDate

 class AddRecyclingContainerMaterialsTest {

@@ -52,7 +51,7 @@ class AddRecyclingContainerMaterialsTest {
                 "check_date:recycling" to "2001-01-01",
                 "recycling:plastic_packaging" to "yes",
                 "recycling:something_else" to "no"
-            ), timestamp = Instant.now().toEpochMilli())
+            ), timestamp = nowAsEpochMilliseconds())
         ))
         assertEquals(1, questType.getApplicableElements(mapData).toList().size)
     }
@@ -64,7 +63,7 @@ class AddRecyclingContainerMaterialsTest {
                 "recycling_type" to "container",
                 "check_date:recycling" to "2001-01-01",
                 "recycling:something_else" to "yes"
-            ), timestamp = Instant.now().toEpochMilli())
+            ), timestamp = nowAsEpochMilliseconds())
         ))
         assertEquals(0, questType.getApplicableElements(mapData).toList().size)
     }
@@ -200,7 +199,7 @@ class AddRecyclingContainerMaterialsTest {
             RecyclingMaterials(listOf(CLOTHES, PAPER)),
             StringMapEntryModify("recycling:paper", "yes", "yes"),
             StringMapEntryModify("recycling:clothes", "yes", "yes"),
-            StringMapEntryAdd("check_date:recycling", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date:recycling", nowAsCheckDateString())
         )
     }

@@ -233,7 +232,7 @@ class AddRecyclingContainerMaterialsTest {
             RecyclingMaterials(listOf(PAPER)),
             StringMapEntryModify("recycling:paper", "no", "yes"),
             StringMapEntryDelete("recycling:check_date", "2000-11-01"),
-            StringMapEntryModify("check_date:recycling", "2000-11-02", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:recycling", "2000-11-02", nowAsCheckDateString()),
             StringMapEntryDelete("recycling:lastcheck", "2000-11-03"),
             StringMapEntryDelete("lastcheck:recycling", "2000-11-04"),
             StringMapEntryDelete("recycling:last_checked", "2000-11-05"),
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopTypeTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopTypeTest.kt
index e8b4dd6e932..bce6358565f 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopTypeTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/shop_type/CheckShopTypeTest.kt
@@ -3,13 +3,12 @@ package de.westnordost.streetcomplete.quests.shop_type
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.node
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate

 class CheckShopTypeTest {
     private val questType = CheckShopType()
@@ -53,7 +52,7 @@ class CheckShopTypeTest {
     @Test fun `apply shop vacant answer`() {
         questType.verifyAnswer(
             IsShopVacant,
-            StringMapEntryAdd("check_date", LocalDate.now().toCheckDateString())
+            StringMapEntryAdd("check_date", nowAsCheckDateString())
         )
     }

@@ -61,7 +60,7 @@ class CheckShopTypeTest {
         questType.verifyAnswer(
             mapOf("check_date" to "already set"),
             IsShopVacant,
-            StringMapEntryModify("check_date", "already set", LocalDate.now().toCheckDateString())
+            StringMapEntryModify("check_date", "already set", nowAsCheckDateString())
         )
     }

@@ -74,7 +73,7 @@ class CheckShopTypeTest {
                 "survey_date" to "d"
             ),
             IsShopVacant,
-            StringMapEntryAdd("check_date", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date", nowAsCheckDateString()),
             StringMapEntryDelete("lastcheck", "a"),
             StringMapEntryDelete("last_checked", "b"),
             StringMapEntryDelete("survey:date", "c"),
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/smoothness/SmoothnessAnswerKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/smoothness/SmoothnessAnswerKtTest.kt
index 837f7a88222..4980d390d6d 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/smoothness/SmoothnessAnswerKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/smoothness/SmoothnessAnswerKtTest.kt
@@ -5,10 +5,9 @@ import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import org.assertj.core.api.Assertions
 import org.junit.Test
-import java.time.LocalDate

 class SmoothnessAnswerKtTest {

@@ -32,7 +31,7 @@ class SmoothnessAnswerKtTest {
                 StringMapEntryModify("smoothness", "excellent", "excellent"),
                 StringMapEntryDelete("smoothness:date", "2000-10-10"),
                 StringMapEntryDelete("surface:grade", "1"),
-                StringMapEntryAdd("check_date:smoothness", LocalDate.now().toCheckDateString())
+                StringMapEntryAdd("check_date:smoothness", nowAsCheckDateString())
             ),
         )
     }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRampTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRampTest.kt
index 5009226b257..258401e4d30 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRampTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/steps_ramp/AddStepsRampTest.kt
@@ -3,10 +3,9 @@ package de.westnordost.streetcomplete.quests.steps_ramp
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import org.junit.Test
-import java.time.LocalDate

 class AddStepsRampTest {

@@ -51,7 +50,7 @@ class AddStepsRampTest {
             ),
             StringMapEntryModify("ramp", "yes", "yes"),
             StringMapEntryAdd("ramp:wheelchair", "yes"),
-            StringMapEntryAdd("check_date:ramp", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date:ramp", nowAsCheckDateString()),
         )
     }

@@ -86,7 +85,7 @@ class AddStepsRampTest {
             StringMapEntryModify("ramp:bicycle", "yes", "yes"),
             StringMapEntryModify("ramp:stroller", "no", "yes"),
             StringMapEntryModify("ramp:wheelchair", "automatic", "yes"),
-            StringMapEntryAdd("check_date:ramp", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date:ramp", nowAsCheckDateString()),
         )
     }

@@ -121,7 +120,7 @@ class AddStepsRampTest {
                 wheelchairRamp = WheelchairRampStatus.NO
             ),
             StringMapEntryModify("ramp", "yes", "yes"),
-            StringMapEntryAdd("check_date:ramp", LocalDate.now().toCheckDateString()),
+            StringMapEntryAdd("check_date:ramp", nowAsCheckDateString()),
             StringMapEntryAdd("ramp:bicycle", "no"),
             StringMapEntryAdd("ramp:stroller", "no"),
             StringMapEntryAdd("ramp:wheelchair", "no"),
diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt
index 4da9007848e..db5f8b01579 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt
@@ -3,13 +3,12 @@ package de.westnordost.streetcomplete.quests.surface
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import de.westnordost.streetcomplete.quests.verifyAnswer
 import de.westnordost.streetcomplete.testutils.way
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
-import java.time.LocalDate

 class AddSidewalkSurfaceTest {
     private val questType = AddSidewalkSurface()
@@ -74,7 +73,7 @@ class AddSidewalkSurfaceTest {
             mapOf("sidewalk:both:surface" to "asphalt", "check_date:sidewalk:surface" to "2000-10-10"),
             SidewalkSurfaceAnswer(SurfaceAnswer(Surface.ASPHALT), SurfaceAnswer(Surface.ASPHALT)),
             StringMapEntryModify("sidewalk:both:surface", "asphalt", "asphalt"),
-            StringMapEntryModify("check_date:sidewalk:surface", "2000-10-10", LocalDate.now().toCheckDateString()),
+            StringMapEntryModify("check_date:sidewalk:surface", "2000-10-10", nowAsCheckDateString()),
         )
     }

diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/surface/SurfaceAnswerKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/surface/SurfaceAnswerKtTest.kt
index fce7854e615..5a160ce483b 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/quests/surface/SurfaceAnswerKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/quests/surface/SurfaceAnswerKtTest.kt
@@ -5,10 +5,9 @@ import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAd
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryChange
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete
 import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify
-import de.westnordost.streetcomplete.osm.toCheckDateString
+import de.westnordost.streetcomplete.osm.nowAsCheckDateString
 import org.assertj.core.api.Assertions
 import org.junit.Test
-import java.time.LocalDate

 internal class SurfaceAnswerKtTest {

@@ -29,7 +28,7 @@ internal class SurfaceAnswerKtTest {
             SurfaceAnswer(Surface.ASPHALT),
             arrayOf(
                 StringMapEntryModify("surface", "asphalt", "asphalt"),
-                StringMapEntryAdd("check_date:surface", LocalDate.now().toCheckDateString())
+                StringMapEntryAdd("check_date:surface", nowAsCheckDateString())
             )
         )
     }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/testutils/TestDataShortcuts.kt b/app/src/test/java/de/westnordost/streetcomplete/testutils/TestDataShortcuts.kt
index a0dc5a99897..db565812596 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/testutils/TestDataShortcuts.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/testutils/TestDataShortcuts.kt
@@ -26,7 +26,7 @@ import de.westnordost.streetcomplete.data.osmtracks.Trackpoint
 import de.westnordost.streetcomplete.data.quest.OsmQuestKey
 import de.westnordost.streetcomplete.data.quest.TestQuestTypeA
 import de.westnordost.streetcomplete.data.user.User
-import java.lang.System.currentTimeMillis
+import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds

 fun p(lat: Double = 0.0, lon: Double = 0.0) = LatLon(lat, lon)

@@ -36,7 +36,7 @@ fun node(
     tags: Map<String, String> = emptyMap(),
     version: Int = 1,
     timestamp: Long? = null
-) = Node(id, pos, tags, version, timestamp ?: currentTimeMillis())
+) = Node(id, pos, tags, version, timestamp ?: nowAsEpochMilliseconds())

 fun way(
     id: Long = 1,
@@ -44,7 +44,7 @@ fun way(
     tags: Map<String, String> = emptyMap(),
     version: Int = 1,
     timestamp: Long? = null
-) = Way(id, nodes, tags, version, timestamp ?: currentTimeMillis())
+) = Way(id, nodes, tags, version, timestamp ?: nowAsEpochMilliseconds())

 fun rel(
     id: Long = 1,
@@ -52,7 +52,7 @@ fun rel(
     tags: Map<String, String> = emptyMap(),
     version: Int = 1,
     timestamp: Long? = null
-) = Relation(id, members.toMutableList(), tags, version, timestamp ?: currentTimeMillis())
+) = Relation(id, members.toMutableList(), tags, version, timestamp ?: nowAsEpochMilliseconds())

 fun member(
     type: ElementType = ElementType.NODE,
diff --git a/app/src/test/java/de/westnordost/streetcomplete/util/ktx/DateTimeTest.kt b/app/src/test/java/de/westnordost/streetcomplete/util/ktx/DateTimeTest.kt
new file mode 100644
index 00000000000..0bbba1ea475
--- /dev/null
+++ b/app/src/test/java/de/westnordost/streetcomplete/util/ktx/DateTimeTest.kt
@@ -0,0 +1,21 @@
+package de.westnordost.streetcomplete.util.ktx
+
+import kotlinx.datetime.Instant
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DateTimeTest {
+    @Test fun `check parsing of ISO timestamp with offset`() {
+        assertEquals(
+            java.time.OffsetDateTime.parse("2007-12-03T10:15:30+01:00").toInstant().toEpochMilli(),
+            Instant.parse("2007-12-03T10:15:30+01:00").toEpochMilliseconds()
+        )
+    }
+
+    @Test fun `check LocalDate round-trip`() {
+        val s = "2010-04-16"
+        assertEquals(s, LocalDate.parse(s).toString())
+    }
+}