Use an 8-digit token for each car so it is easy to share.

This commit is contained in:
RallyBlock committed 2026-10-22 20:54:56 -07:00
1 parent 0224e8f395
commit b95cbefeab
6 files changed
+42 -15

No files matched your search

@@ -253,7 +253,7 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL(
"""
INSERT INTO cars (carName, raceClass, discipline, token, updatedAt, syncRev, deleted, pending)
VALUES ('$DEFAULT_CAR', '$DEFAULT_CLASS', '$DEFAULT_DISCIPLINE', lower(hex(randomblob(32))), 1, 0, 0, 1)
VALUES ('$DEFAULT_CAR', '$DEFAULT_CLASS', '$DEFAULT_DISCIPLINE', printf('%08d', abs(random()) % 100000000), 1, 0, 0, 1)
""".trimIndent(),
)
}
@@ -132,6 +132,15 @@ interface RaceDao {
@Query("SELECT * FROM cars WHERE id = :id")
suspend fun carById(id: Long): Car?
@Query("UPDATE race_nights SET pending = 1 WHERE carId = :carId")
suspend fun markNightsPending(carId: Long)
@Query("UPDATE runs SET pending = 1 WHERE nightId IN (SELECT id FROM race_nights WHERE carId = :carId)")
suspend fun markRunsPending(carId: Long)
@Query("UPDATE setup_sheets SET pending = 1 WHERE carId = :carId")
suspend fun markSheetsPending(carId: Long)
@Query("SELECT * FROM cars WHERE token = :token LIMIT 1")
suspend fun carByToken(token: String): Car?
@@ -1,5 +1,6 @@
package com.racingapp.pitlog.data
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.UUID
import kotlinx.coroutines.flow.Flow
@@ -24,28 +25,39 @@ suspend fun ensureCars(dao: RaceDao, preferences: SyncPreferences) {
preferences.selectedCarId = id
return
}
val first = cars.firstOrNull { !it.deleted } ?: cars.first()
if (first.token.isBlank()) {
val kept = preferences.token.trim().ifBlank { newCarToken() }
val rev = if (kept == preferences.token.trim() && first.syncRev == 0L) preferences.rev else first.syncRev
dao.updateCar(first.copy(token = kept, syncRev = rev, pending = true))
for (car in cars) {
if (isEightDigitToken(car.token)) continue
val source = car.token.ifBlank { preferences.token.trim() }
val next = if (source.isBlank()) newCarToken() else eightDigitToken(source)
dao.updateCar(car.copy(token = next, syncRev = 0, pending = true))
dao.markNightsPending(car.id)
dao.markRunsPending(car.id)
dao.markSheetsPending(car.id)
}
val first = cars.firstOrNull { !it.deleted } ?: cars.first()
val selected = preferences.selectedCarId
if (selected == 0L || cars.none { it.id == selected && !it.deleted }) {
preferences.selectedCarId = first.id
}
}
fun newCarToken(): String {
val bytes = ByteArray(32)
SecureRandom().nextBytes(bytes)
return bytes.joinToString("") { "%02x".format(it) }
fun newCarToken(): String = "%08d".format(SecureRandom().nextInt(100_000_000))
fun eightDigitToken(current: String): String {
val digest = MessageDigest.getInstance("SHA-256").digest(current.toByteArray(Charsets.UTF_8))
var value = 0L
for (index in 0 until 4) {
value = (value shl 8) or (digest[index].toLong() and 0xff)
}
return "%08d".format(value % 100_000_000L)
}
fun isEightDigitToken(token: String): Boolean = token.length == 8 && token.all { it.isDigit() }
fun cleanCarToken(raw: String): String {
val line = raw.trim().lineSequence().firstOrNull().orEmpty().trim()
val withoutLabel = if (line.startsWith("token:", ignoreCase = true)) line.substringAfter(":").trim() else line
return withoutLabel.lowercase()
return withoutLabel.filter { !it.isWhitespace() }.lowercase()
}
class Repository(
@@ -125,7 +137,7 @@ class Repository(
suspend fun joinCar(raw: String): String {
val token = cleanCarToken(raw)
if (token.length < 16 || token.any { it !in '0'..'9' && it !in 'a'..'f' }) {
if (!isEightDigitToken(token)) {
return "That token doesn't look right."
}
val existing = dao.carByToken(token)
@@ -230,7 +230,7 @@ fun SettingsScreen(onBack: () -> Unit) {
)
SectionLabel("This car's token")
Text(
"Share this token with another phone, then on that phone use Add a car from a token. That phone syncs this car. A different car gets a different token, so it can sync with different phones.",
"This token is 8 digits. Share it with another phone, then on that phone use Add a car from a token. That phone syncs this car. A different car gets a different token.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
SelectionContainer {