diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/androidApp/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 4794d161..557d2e40 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -91,7 +91,7 @@ class TimerService : Service(), KoinComponent { private val cs by lazy { stateRepository.colorScheme } - private lateinit var notificationStyle: NotificationCompat.ProgressStyle + private var notificationStyle = NotificationCompat.ProgressStyle() override fun onBind(intent: Intent?): IBinder? { return null diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index fb9612a6..8a58930c 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -206,6 +206,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { val history = listOf( Stat( date = LocalDate.of(2026, 3, 12), + deviceId = "0", focusTimeQ1 = 1617943 + 7200000, focusTimeQ2 = 5704591, focusTimeQ3 = 556490, @@ -214,6 +215,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 13), + deviceId = "0", focusTimeQ1 = 1128282 + 7200000, focusTimeQ2 = 4590524, focusTimeQ3 = 7747202, @@ -222,6 +224,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 14), + deviceId = "0", focusTimeQ1 = 1418079 + 7200000, focusTimeQ2 = 8141785, focusTimeQ3 = 5208864, @@ -230,6 +233,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 15), + deviceId = "0", focusTimeQ1 = 38960 + 7200000, focusTimeQ2 = 9544172, focusTimeQ3 = 2216626, @@ -238,6 +242,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 16), + deviceId = "0", focusTimeQ1 = 948108 + 7200000, focusTimeQ2 = 7715257, focusTimeQ3 = 648629, @@ -246,6 +251,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 17), + deviceId = "0", focusTimeQ1 = 1673932 + 7200000, focusTimeQ2 = 7368028, focusTimeQ3 = 6028910, @@ -254,6 +260,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 18), + deviceId = "0", focusTimeQ1 = 435688 + 7200000, focusTimeQ2 = 9487983, focusTimeQ3 = 248276, @@ -262,6 +269,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 19), + deviceId = "0", focusTimeQ1 = 1579291 + 7200000, focusTimeQ2 = 3743344, focusTimeQ3 = 3383617, @@ -270,6 +278,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 20), + deviceId = "0", focusTimeQ1 = 522247 + 7200000, focusTimeQ2 = 7156785, focusTimeQ3 = 5190730, @@ -278,6 +287,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 21), + deviceId = "0", focusTimeQ1 = 310048 + 7200000, focusTimeQ2 = 5901959, focusTimeQ3 = 441673, @@ -286,6 +296,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 22), + deviceId = "0", focusTimeQ1 = 1200000 + 7200000, focusTimeQ2 = 4000000, focusTimeQ3 = 3000000, @@ -294,6 +305,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 23), + deviceId = "0", focusTimeQ1 = 500000 + 7200000, focusTimeQ2 = 8000000, focusTimeQ3 = 1000000, @@ -302,6 +314,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 24), + deviceId = "0", focusTimeQ1 = 2000000 + 7200000, focusTimeQ2 = 2000000, focusTimeQ3 = 2000000, @@ -310,6 +323,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 25), + deviceId = "0", focusTimeQ1 = 0 + 7200000, focusTimeQ2 = 10000000, focusTimeQ3 = 0, @@ -318,6 +332,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ), Stat( date = LocalDate.of(2026, 3, 26), + deviceId = "0", focusTimeQ1 = 3000000 + 7200000, focusTimeQ2 = 3000000, focusTimeQ3 = 3000000, diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index d2dff0f7..53ddb414 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -84,7 +84,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { ) { val statRepository: StatRepository = get() val stat = statRepository.getTodayStat().first() - ?: Stat(LocalDate.now(), 0, 0, 0, 0, 0) + ?: Stat(LocalDate.now(), "0", 0, 0, 0, 0, 0) provideContent { key(LocalSize.current) { @@ -205,6 +205,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { Content( Stat( date = LocalDate.of(2026, 3, 12), + deviceId = "0", focusTimeQ1 = 1617943, focusTimeQ2 = 5704591, focusTimeQ3 = 556490, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d9ef65c5..5afd9339 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ junit = "4.13.2" junitVersion = "1.3.0" kotlin = "2.3.20" kotlinxCoroutines = "1.10.2" +kotlinxSerializationJson = "1.11.0" ksp = "2.3.6" lifecycleRuntime = "2.10.0" materialKolor = "4.1.1" @@ -64,6 +65,7 @@ filekit-core = { module = "io.github.vinceglb:filekit-core", version.ref = "file filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekitDialogsCompose" } jlayer-player = { module = "dev.mccue:jlayer-player", version.ref = "jlayerPlayer" } junit = { group = "junit", name = "junit", version.ref = "junit" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } revenuecat-purchases = { module = "com.revenuecat.purchases:purchases", version.ref = "revenuecat" } revenuecat-purchases-ui = { module = "com.revenuecat.purchases:purchases-ui", version.ref = "revenuecat" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b9214057..401b9e98 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -79,6 +79,7 @@ kotlin { implementation(libs.koin.compose.viewmodel) implementation(libs.androidx.room.runtime) + implementation(libs.kotlinx.serialization.json) implementation(libs.vico.compose.m3) implementation(libs.material.kolor) diff --git a/shared/schemas/org.nsh07.pomodoro.data.AppDatabase/3.json b/shared/schemas/org.nsh07.pomodoro.data.AppDatabase/3.json new file mode 100644 index 00000000..6936a843 --- /dev/null +++ b/shared/schemas/org.nsh07.pomodoro.data.AppDatabase/3.json @@ -0,0 +1,146 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "710379259efe0b7d0cd521b4698fc0c5", + "entities": [ + { + "tableName": "int_preference", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "boolean_preference", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "string_preference", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "stat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `focusTimeQ1` INTEGER NOT NULL, `focusTimeQ2` INTEGER NOT NULL, `focusTimeQ3` INTEGER NOT NULL, `focusTimeQ4` INTEGER NOT NULL, `breakTime` INTEGER NOT NULL, PRIMARY KEY(`date`, `deviceId`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "focusTimeQ1", + "columnName": "focusTimeQ1", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "focusTimeQ2", + "columnName": "focusTimeQ2", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "focusTimeQ3", + "columnName": "focusTimeQ3", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "focusTimeQ4", + "columnName": "focusTimeQ4", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "breakTime", + "columnName": "breakTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "date", + "deviceId" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '710379259efe0b7d0cd521b4698fc0c5')" + ] + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/nsh07/pomodoro/data/AndroidBackupRestoreManager.kt b/shared/src/androidMain/kotlin/org/nsh07/pomodoro/data/AndroidBackupRestoreManager.kt index 909d144c..17c04128 100644 --- a/shared/src/androidMain/kotlin/org/nsh07/pomodoro/data/AndroidBackupRestoreManager.kt +++ b/shared/src/androidMain/kotlin/org/nsh07/pomodoro/data/AndroidBackupRestoreManager.kt @@ -22,10 +22,14 @@ import android.content.Intent import android.provider.DocumentsContract import androidx.core.net.toUri import androidx.room.RoomRawQuery +import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.cacheDir import io.github.vinceglb.filekit.path +import io.github.vinceglb.filekit.writeString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import org.nsh07.pomodoro.BuildKonfig import java.io.File import java.io.FileInputStream @@ -34,9 +38,13 @@ import kotlin.time.Clock class AndroidBackupRestoreManager( private val database: AppDatabase, + private val statDao: StatDao, private val systemDao: SystemDao, - private val context: Context + private val context: Context, + deviceIdStore: DeviceIdStore ) : BackupRestoreManager { + val deviceId = deviceIdStore.deviceId + override suspend fun performBackup(directory: PlatformFile) { withContext(Dispatchers.IO) { systemDao.checkpoint(RoomRawQuery("PRAGMA wal_checkpoint(full)")) @@ -86,6 +94,37 @@ class AndroidBackupRestoreManager( } } + override suspend fun exportSyncFile(): PlatformFile { + val stats = statDao.getAllRows() + + val payload = SyncPayload( + schemaVersion = DB_SCHEMA_VERSION, + exportedAt = System.currentTimeMillis(), + deviceId = deviceId.value, + stats = stats + ) + + val outputFile = + PlatformFile(FileKit.cacheDir, "tomato-backup-${Clock.System.now()}.tomatoSync") + + withContext(Dispatchers.IO) { + val content = Json.encodeToString(payload) + outputFile.writeString(content) + } + + return outputFile + } + + override suspend fun importSyncFile(file: PlatformFile?) { + if (file == null) return + withContext(Dispatchers.IO) { + val bytes = File(file.path).readBytes() + val content = bytes.decodeToString() + val payload = Json.decodeFromString(content) + statDao.insertStatsIfNewer(payload.stats) + } + } + override fun restartApp() { val packageManager = context.packageManager val intent = packageManager.getLaunchIntentForPackage(context.packageName) diff --git a/shared/src/androidMain/kotlin/org/nsh07/pomodoro/di/modules.kt b/shared/src/androidMain/kotlin/org/nsh07/pomodoro/di/modules.kt index 6ba0b1b0..a6519b91 100644 --- a/shared/src/androidMain/kotlin/org/nsh07/pomodoro/di/modules.kt +++ b/shared/src/androidMain/kotlin/org/nsh07/pomodoro/di/modules.kt @@ -19,6 +19,8 @@ package org.nsh07.pomodoro.di import android.content.Context import androidx.room.Room +import org.koin.core.module.dsl.createdAtStart +import org.koin.core.module.dsl.withOptions import org.koin.dsl.bind import org.koin.dsl.module import org.koin.plugin.module.dsl.create @@ -28,12 +30,15 @@ import org.nsh07.pomodoro.BuildKonfig import org.nsh07.pomodoro.data.AndroidBackupRestoreManager import org.nsh07.pomodoro.data.AppDatabase import org.nsh07.pomodoro.data.BackupRestoreManager +import org.nsh07.pomodoro.data.DeviceIdStore +import org.nsh07.pomodoro.data.Migration2to3 import org.nsh07.pomodoro.ui.settingsScreen.screens.backupRestore.viewModel.BackupRestoreViewModel import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel val dbModule = module { + single() withOptions { createdAtStart() } single { create(::createDatabase) } single { get().preferenceDao() } single { get().statDao() } @@ -51,10 +56,16 @@ val androidModule = module { single() bind BackupRestoreManager::class } -private fun createDatabase(context: Context): AppDatabase { - return Room.databaseBuilder( - context, - AppDatabase::class.java, - BuildKonfig.DATABASE_NAME - ).build() +private fun createDatabase( + context: Context, + deviceIdStore: DeviceIdStore +): AppDatabase { + return Room + .databaseBuilder( + context, + AppDatabase::class.java, + BuildKonfig.DATABASE_NAME + ) + .addMigrations(Migration2to3(deviceIdStore::getDeviceId)) + .build() } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt index ba3da5d6..5e522e39 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt @@ -22,9 +22,11 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +const val DB_SCHEMA_VERSION = 3 + @Database( entities = [IntPreference::class, BooleanPreference::class, StringPreference::class, Stat::class], - version = 2, + version = DB_SCHEMA_VERSION, autoMigrations = [ AutoMigration(from = 1, to = 2) ] diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/BackupRestoreManager.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/BackupRestoreManager.kt index 54570f19..74abc874 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/BackupRestoreManager.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/BackupRestoreManager.kt @@ -18,10 +18,22 @@ package org.nsh07.pomodoro.data import io.github.vinceglb.filekit.PlatformFile +import kotlinx.serialization.Serializable interface BackupRestoreManager { suspend fun performBackup(directory: PlatformFile) suspend fun performRestore(file: PlatformFile?) + suspend fun exportSyncFile(): PlatformFile + suspend fun importSyncFile(file: PlatformFile?) + fun restartApp() -} \ No newline at end of file +} + +@Serializable +data class SyncPayload( + val schemaVersion: Int = DB_SCHEMA_VERSION, + val exportedAt: Long, + val deviceId: String, + val stats: List +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/DeviceIdStore.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/DeviceIdStore.kt new file mode 100644 index 00000000..66deb6a5 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/DeviceIdStore.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.data + +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.databasesDir +import io.github.vinceglb.filekit.exists +import io.github.vinceglb.filekit.readString +import io.github.vinceglb.filekit.writeString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +class DeviceIdStore { + val deviceId = MutableStateFlow("fallback-id") + + init { + CoroutineScope(Dispatchers.IO).launch { + deviceId.update { getDeviceId() } + } + } + + @OptIn(ExperimentalUuidApi::class) + suspend fun getDeviceId(): String { + val idFile = PlatformFile(FileKit.databasesDir, "device_id") + + if (idFile.exists()) return idFile.readString() + + val id = Uuid.random().toHexString() + idFile.writeString(id) + + return id + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Migrations.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Migrations.kt new file mode 100644 index 00000000..64cdb305 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Migrations.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.data + +import androidx.room.migration.Migration +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.execSQL +import kotlinx.coroutines.runBlocking + +class Migration2to3( + private val getDeviceId: suspend () -> String +) : Migration(2, 3) { + override fun migrate(connection: SQLiteConnection) { + val currentTimeMillis = System.currentTimeMillis() + val currentDeviceId = + runBlocking { getDeviceId() } // must run blocking to get correct device id from disk + + connection.execSQL( + """ + CREATE TABLE IF NOT EXISTS `stat_new` ( + `date` TEXT NOT NULL, + `deviceId` TEXT NOT NULL, + `updatedAt` INTEGER NOT NULL, + `focusTimeQ1` INTEGER NOT NULL, + `focusTimeQ2` INTEGER NOT NULL, + `focusTimeQ3` INTEGER NOT NULL, + `focusTimeQ4` INTEGER NOT NULL, + `breakTime` INTEGER NOT NULL, + PRIMARY KEY(`date`, `deviceId`) + ) + """.trimIndent() + ) + + connection.execSQL( + """ + INSERT INTO `stat_new` ( + `date`, `deviceId`, `updatedAt`, `focusTimeQ1`, `focusTimeQ2`, + `focusTimeQ3`, `focusTimeQ4`, `breakTime` + ) + SELECT + `date`, '$currentDeviceId', $currentTimeMillis, `focusTimeQ1`, `focusTimeQ2`, + `focusTimeQ3`, `focusTimeQ4`, `breakTime` + FROM `stat` + """.trimIndent() + ) + + connection.execSQL("DROP TABLE `stat`") + connection.execSQL("ALTER TABLE `stat_new` RENAME TO `stat`") + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Stat.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Stat.kt index 5016d23c..c9157f03 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Stat.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/Stat.kt @@ -19,7 +19,12 @@ package org.nsh07.pomodoro.data import androidx.compose.runtime.Immutable import androidx.room.Entity -import androidx.room.PrimaryKey +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.time.LocalDate /** @@ -28,15 +33,21 @@ import java.time.LocalDate * separately for later analysis (e.g. for showing which parts of the day are most productive). */ @Immutable -@Entity(tableName = "stat") +@Serializable +@Entity( + tableName = "stat", + primaryKeys = ["date", "deviceId"] +) data class Stat( - @PrimaryKey + @Serializable(with = LocalDateSerializer::class) val date: LocalDate, + val deviceId: String, val focusTimeQ1: Long, val focusTimeQ2: Long, val focusTimeQ3: Long, val focusTimeQ4: Long, - val breakTime: Long + val breakTime: Long, + val updatedAt: Long = System.currentTimeMillis() ) { fun totalFocusTime() = focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 } @@ -47,4 +58,17 @@ data class StatTime( val focusTimeQ3: Long, val focusTimeQ4: Long, val breakTime: Long, -) \ No newline at end of file +) + +private class LocalDateSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor( + "java.time.LocalDate", + PrimitiveKind.STRING + ) + + override fun serialize(encoder: Encoder, value: LocalDate) = + encoder.encodeString(value.toString()) + + override fun deserialize(decoder: Decoder): LocalDate = + LocalDate.parse(decoder.decodeString()) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatDao.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatDao.kt index 214f8f38..a262c035 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatDao.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatDao.kt @@ -19,62 +19,120 @@ package org.nsh07.pomodoro.data import androidx.room.Dao import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import kotlinx.coroutines.flow.Flow import java.time.LocalDate @Dao interface StatDao { - @Insert(onConflict = REPLACE) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertStat(stat: Stat) - @Query("UPDATE stat SET focusTimeQ1 = focusTimeQ1 + :focusTime WHERE date = :date") - suspend fun addFocusTimeQ1(date: LocalDate, focusTime: Long) - @Query("UPDATE stat SET focusTimeQ2 = focusTimeQ2 + :focusTime WHERE date = :date") - suspend fun addFocusTimeQ2(date: LocalDate, focusTime: Long) + @Query("UPDATE stat SET focusTimeQ1 = focusTimeQ1 + :focusTime, updatedAt = :updatedAt WHERE date = :date AND deviceId = :deviceId") + suspend fun addFocusTimeQ1(date: LocalDate, deviceId: String, focusTime: Long, updatedAt: Long) - @Query("UPDATE stat SET focusTimeQ3 = focusTimeQ3 + :focusTime WHERE date = :date") - suspend fun addFocusTimeQ3(date: LocalDate, focusTime: Long) + @Query("UPDATE stat SET focusTimeQ2 = focusTimeQ2 + :focusTime, updatedAt = :updatedAt WHERE date = :date AND deviceId = :deviceId") + suspend fun addFocusTimeQ2(date: LocalDate, deviceId: String, focusTime: Long, updatedAt: Long) - @Query("UPDATE stat SET focusTimeQ4 = focusTimeQ4 + :focusTime WHERE date = :date") - suspend fun addFocusTimeQ4(date: LocalDate, focusTime: Long) + @Query("UPDATE stat SET focusTimeQ3 = focusTimeQ3 + :focusTime, updatedAt = :updatedAt WHERE date = :date AND deviceId = :deviceId") + suspend fun addFocusTimeQ3(date: LocalDate, deviceId: String, focusTime: Long, updatedAt: Long) - @Query("UPDATE stat SET breakTime = breakTime + :breakTime WHERE date = :date") - suspend fun addBreakTime(date: LocalDate, breakTime: Long) + @Query("UPDATE stat SET focusTimeQ4 = focusTimeQ4 + :focusTime, updatedAt = :updatedAt WHERE date = :date AND deviceId = :deviceId") + suspend fun addFocusTimeQ4(date: LocalDate, deviceId: String, focusTime: Long, updatedAt: Long) - @Query("SELECT * FROM stat WHERE date = :date") + @Query("UPDATE stat SET breakTime = breakTime + :breakTime, updatedAt = :updatedAt WHERE date = :date AND deviceId = :deviceId") + suspend fun addBreakTime(date: LocalDate, deviceId: String, breakTime: Long, updatedAt: Long) + + @Query( + """ + SELECT + date, + 'merged' AS deviceId, + SUM(focusTimeQ1) AS focusTimeQ1, + SUM(focusTimeQ2) AS focusTimeQ2, + SUM(focusTimeQ3) AS focusTimeQ3, + SUM(focusTimeQ4) AS focusTimeQ4, + SUM(breakTime) AS breakTime, + MAX(updatedAt) AS updatedAt + FROM stat + WHERE date = :date + GROUP BY date + """ + ) fun getStat(date: LocalDate): Flow - @Query("SELECT * FROM stat ORDER BY date DESC LIMIT :n") + @Query( + """ + SELECT + date, + 'merged' AS deviceId, + SUM(focusTimeQ1) AS focusTimeQ1, + SUM(focusTimeQ2) AS focusTimeQ2, + SUM(focusTimeQ3) AS focusTimeQ3, + SUM(focusTimeQ4) AS focusTimeQ4, + SUM(breakTime) AS breakTime, + MAX(updatedAt) AS updatedAt + FROM stat + GROUP BY date + ORDER BY date DESC + LIMIT :n + """ + ) fun getLastNDaysStats(n: Int): Flow> + @Query("SELECT * FROM stat") + suspend fun getAllRows(): List + @Query( - "SELECT " + - " CAST(AVG(focusTimeQ1) AS INTEGER) AS focusTimeQ1," + - " CAST(AVG(focusTimeQ2) AS INTEGER) AS focusTimeQ2," + - " CAST(AVG(focusTimeQ3) AS INTEGER) AS focusTimeQ3," + - " CAST(AVG(focusTimeQ4) AS INTEGER) AS focusTimeQ4," + - " CAST(AVG(breakTime) AS INTEGER) AS breakTime " + - "FROM (" + - " SELECT * FROM stat" + - " ORDER BY date DESC" + - " LIMIT :n" + - ")" + - "WHERE focusTimeQ1 > 0 OR focusTimeQ2 > 0 OR focusTimeQ3 > 0 OR focusTimeQ4 > 0" + """ + SELECT + CAST(AVG(focusTimeQ1) AS INTEGER) AS focusTimeQ1, + CAST(AVG(focusTimeQ2) AS INTEGER) AS focusTimeQ2, + CAST(AVG(focusTimeQ3) AS INTEGER) AS focusTimeQ3, + CAST(AVG(focusTimeQ4) AS INTEGER) AS focusTimeQ4, + CAST(AVG(breakTime) AS INTEGER) AS breakTime + FROM ( + SELECT + SUM(focusTimeQ1) AS focusTimeQ1, + SUM(focusTimeQ2) AS focusTimeQ2, + SUM(focusTimeQ3) AS focusTimeQ3, + SUM(focusTimeQ4) AS focusTimeQ4, + SUM(breakTime) AS breakTime + FROM stat + GROUP BY date + ORDER BY date DESC + LIMIT :n + ) + WHERE focusTimeQ1 > 0 OR focusTimeQ2 > 0 OR focusTimeQ3 > 0 OR focusTimeQ4 > 0 + """ ) fun getLastNDaysAvgStats(n: Int): Flow - @Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)") + @Query("SELECT EXISTS (SELECT 1 FROM stat WHERE date = :date)") suspend fun statExists(date: LocalDate): Boolean @Query("SELECT date FROM stat ORDER BY date DESC LIMIT 1") suspend fun getLastDate(): LocalDate? - @Query("SELECT SUM(focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4) FROM STAT") + @Query("SELECT SUM(focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4) FROM stat") fun getAllTimeTotalFocusTime(): Flow @Query("DELETE FROM stat") suspend fun clearAll() + + @Query("SELECT * FROM stat WHERE date = :date AND deviceId = :deviceId") + suspend fun getStatByDateAndDevice(date: LocalDate, deviceId: String): Stat? + + @Transaction + suspend fun insertStatsIfNewer(stats: List) { + stats.forEach { stat -> + val existing = getStatByDateAndDevice(stat.date, stat.deviceId) + if (existing == null || stat.updatedAt > existing.updatedAt) { + insertStat(stat) + } + } + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatRepository.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatRepository.kt index 9bc6fa85..5e8cee45 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatRepository.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StatRepository.kt @@ -29,7 +29,7 @@ import java.time.LocalTime * ViewModel */ interface StatRepository { - suspend fun insertStat(stat: Stat) + suspend fun insertStat(date: LocalDate, stat: StatTime) suspend fun addFocusTime(focusTime: Long) @@ -53,48 +53,63 @@ interface StatRepository { */ class AppStatRepository( private val statDao: StatDao, - private val ioDispatcher: CoroutineDispatcher + private val ioDispatcher: CoroutineDispatcher, + deviceIdStore: DeviceIdStore ) : StatRepository { - override suspend fun insertStat(stat: Stat) = statDao.insertStat(stat) + private val deviceId = deviceIdStore.deviceId + + override suspend fun insertStat(date: LocalDate, stat: StatTime) = + statDao.insertStat( + Stat( + date, + deviceId.value, + stat.focusTimeQ1, + stat.focusTimeQ2, + stat.focusTimeQ3, + stat.focusTimeQ4, + stat.breakTime + ) + ) override suspend fun addFocusTime(focusTime: Long) = withContext(ioDispatcher) { val currentDate = LocalDate.now() val currentTime = LocalTime.now().toSecondOfDay() val secondsInDay = 24 * 60 * 60 + val updatedAt = System.currentTimeMillis() if (statDao.statExists(currentDate)) { when (currentTime) { in 0..(secondsInDay / 4) -> - statDao.addFocusTimeQ1(currentDate, focusTime) + statDao.addFocusTimeQ1(currentDate, deviceId.value, focusTime, updatedAt) in (secondsInDay / 4)..(secondsInDay / 2) -> - statDao.addFocusTimeQ2(currentDate, focusTime) + statDao.addFocusTimeQ2(currentDate, deviceId.value, focusTime, updatedAt) in (secondsInDay / 2)..(3 * secondsInDay / 4) -> - statDao.addFocusTimeQ3(currentDate, focusTime) + statDao.addFocusTimeQ3(currentDate, deviceId.value, focusTime, updatedAt) - else -> statDao.addFocusTimeQ4(currentDate, focusTime) + else -> statDao.addFocusTimeQ4(currentDate, deviceId.value, focusTime, updatedAt) } } else { when (currentTime) { in 0..(secondsInDay / 4) -> statDao.insertStat( - Stat(currentDate, focusTime, 0, 0, 0, 0) + Stat(currentDate, deviceId.value, focusTime, 0, 0, 0, 0) ) in (secondsInDay / 4)..(secondsInDay / 2) -> statDao.insertStat( - Stat(currentDate, 0, focusTime, 0, 0, 0) + Stat(currentDate, deviceId.value, 0, focusTime, 0, 0, 0) ) in (secondsInDay / 2)..(3 * secondsInDay / 4) -> statDao.insertStat( - Stat(currentDate, 0, 0, focusTime, 0, 0) + Stat(currentDate, deviceId.value, 0, 0, focusTime, 0, 0) ) else -> statDao.insertStat( - Stat(currentDate, 0, 0, 0, focusTime, 0) + Stat(currentDate, deviceId.value, 0, 0, 0, focusTime, 0) ) } } @@ -102,10 +117,11 @@ class AppStatRepository( override suspend fun addBreakTime(breakTime: Long) = withContext(ioDispatcher) { val currentDate = LocalDate.now() + val updatedAt = System.currentTimeMillis() if (statDao.statExists(currentDate)) { - statDao.addBreakTime(currentDate, breakTime) + statDao.addBreakTime(currentDate, deviceId.value, breakTime, updatedAt) } else { - statDao.insertStat(Stat(currentDate, 0, 0, 0, 0, breakTime)) + statDao.insertStat(Stat(currentDate, deviceId.value, updatedAt, 0, 0, 0, 0, breakTime)) } } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/FocusHistoryCalendar.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/FocusHistoryCalendar.kt index 4fc95b57..e50623fa 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/FocusHistoryCalendar.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/FocusHistoryCalendar.kt @@ -239,9 +239,11 @@ private fun FocusHistoryCalendarPreview() { val random = Random.nextInt() % 3 if (random == 0) Stat( - date, 0, 0, 0, 0, 0 + date, "0", 0, 0, 0, 0, 0, 0 ) else Stat( date = date, + deviceId = "0", + updatedAt = 0, focusTimeQ1 = quarterTime, focusTimeQ2 = quarterTime, focusTimeQ3 = quarterTime, diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/HeatmapWithWeekLabels.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/HeatmapWithWeekLabels.kt index 55553fed..cb97e3ef 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/HeatmapWithWeekLabels.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/components/HeatmapWithWeekLabels.kt @@ -201,7 +201,8 @@ fun HeatmapWithWeekLabelsPreview() { buildList { (0..93).forEach { index -> val date = startDate.plusDays(index.toLong()) - val focusStat = Stat(date, index % 10L / 2, 0, 0, 0, 0) // Varying focus durations + val focusStat = + Stat(date, "0", 0, index % 10L / 2, 0, 0, 0, 0) // Varying focus durations if (date.month != date.minusDays(1).month && index > 0) repeat(7) { add(null) } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt index e2cd0476..2088c508 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository +import org.nsh07.pomodoro.data.StatTime import org.nsh07.pomodoro.di.AppInfo import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.utils.OS @@ -294,8 +295,8 @@ class StatsViewModel( while (it.isBefore(today)) { statRepository.insertStat( - Stat( - it, + it, + StatTime( (0..30 * 60 * 1000L).random(), (1 * 60 * 60 * 1000L..3 * 60 * 60 * 1000L).random(), (0..3 * 60 * 60 * 1000L).random(), diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 4b9d5d60..8b753f3a 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -31,8 +31,8 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository +import org.nsh07.pomodoro.data.StatTime import org.nsh07.pomodoro.data.StateRepository import org.nsh07.pomodoro.service.TimerHelper import org.nsh07.pomodoro.ui.Screen @@ -43,7 +43,7 @@ import java.time.temporal.ChronoUnit class TimerViewModel( private val timerHelper: TimerHelper, private val stateRepository: StateRepository, - private val statRepository: StatRepository + private val statRepository: StatRepository, ) : ViewModel() { val rootBackstack = mutableStateListOf(Screen.Timer) @@ -63,10 +63,10 @@ class TimerViewModel( if (lastDate != null) { while (ChronoUnit.DAYS.between(lastDate, today) > 0) { lastDate = lastDate?.plusDays(1) - statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0)) + statRepository.insertStat(lastDate!!, StatTime(0, 0, 0, 0, 0)) } } else { - statRepository.insertStat(Stat(today, 0, 0, 0, 0, 0)) + statRepository.insertStat(today, StatTime(0, 0, 0, 0, 0)) } delay(1500) diff --git a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/AppWindow.kt b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/AppWindow.kt index 5fa91bef..1e92699f 100644 --- a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/AppWindow.kt +++ b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/AppWindow.kt @@ -102,8 +102,7 @@ fun ApplicationScope.AppWindow( title = stringResource(Res.string.app_name), icon = painterResource(Res.drawable.logo), undecorated = customWindowDecorsEnabled, - transparent = customWindowDecorsEnabled, - alwaysOnTop = BuildKonfig.DEBUG + transparent = customWindowDecorsEnabled ) { if (isMacOS && settingsState.customWindowDecor) { window.rootPane.apply { diff --git a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/data/DesktopBackupRestoreManager.kt b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/data/DesktopBackupRestoreManager.kt index 5827099e..dc565791 100644 --- a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/data/DesktopBackupRestoreManager.kt +++ b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/data/DesktopBackupRestoreManager.kt @@ -20,10 +20,13 @@ package org.nsh07.pomodoro.data import androidx.room.RoomRawQuery import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.cacheDir import io.github.vinceglb.filekit.databasesDir import io.github.vinceglb.filekit.path +import io.github.vinceglb.filekit.writeString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import org.nsh07.pomodoro.BuildKonfig import java.io.File import kotlin.system.exitProcess @@ -31,8 +34,12 @@ import kotlin.time.Clock class DesktopBackupRestoreManager( private val database: AppDatabase, - private val systemDao: SystemDao + private val statDao: StatDao, + private val systemDao: SystemDao, + deviceIdStore: DeviceIdStore ) : BackupRestoreManager { + val deviceId = deviceIdStore.deviceId + override suspend fun performBackup(directory: PlatformFile) { withContext(Dispatchers.IO) { systemDao.checkpoint(RoomRawQuery("PRAGMA wal_checkpoint(full)")) @@ -63,6 +70,37 @@ class DesktopBackupRestoreManager( } } + override suspend fun exportSyncFile(): PlatformFile { + val stats = statDao.getAllRows() + + val payload = SyncPayload( + schemaVersion = DB_SCHEMA_VERSION, + exportedAt = System.currentTimeMillis(), + deviceId = deviceId.value, + stats = stats + ) + + val outputFile = + PlatformFile(FileKit.cacheDir, "tomato-backup-${Clock.System.now()}.tomatoSync") + + withContext(Dispatchers.IO) { + val content = Json.encodeToString(payload) + outputFile.writeString(content) + } + + return outputFile + } + + override suspend fun importSyncFile(file: PlatformFile?) { + if (file == null) return + withContext(Dispatchers.IO) { + val bytes = File(file.path).readBytes() + val content = bytes.decodeToString() + val payload = Json.decodeFromString(content) + statDao.insertStatsIfNewer(payload.stats) + } + } + override fun restartApp() { try { val processInfo = ProcessHandle.current().info() diff --git a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/di/desktopModules.kt b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/di/desktopModules.kt index ed021eaf..dfa19f78 100644 --- a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/di/desktopModules.kt +++ b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/di/desktopModules.kt @@ -29,6 +29,8 @@ import io.github.vinceglb.filekit.databasesDir import io.github.vinceglb.filekit.path import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import org.koin.core.module.dsl.createdAtStart +import org.koin.core.module.dsl.withOptions import org.koin.dsl.bind import org.koin.dsl.module import org.koin.plugin.module.dsl.create @@ -41,6 +43,8 @@ import org.nsh07.pomodoro.data.AppPreferenceRepository import org.nsh07.pomodoro.data.AppStatRepository import org.nsh07.pomodoro.data.BackupRestoreManager import org.nsh07.pomodoro.data.DesktopBackupRestoreManager +import org.nsh07.pomodoro.data.DeviceIdStore +import org.nsh07.pomodoro.data.Migration2to3 import org.nsh07.pomodoro.data.PreferenceRepository import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.StateRepository @@ -55,6 +59,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel import java.io.File val dbModule = module { + single() withOptions { createdAtStart() } single { create(::createDatabase) } single { get().preferenceDao() } single { get().statDao() } @@ -101,12 +106,13 @@ val flavorUiModule = module { } } -private fun createDatabase(): AppDatabase { +private fun createDatabase(deviceIdStore: DeviceIdStore): AppDatabase { val dbFile = File(FileKit.databasesDir.path, BuildKonfig.DATABASE_NAME) return Room .databaseBuilder(name = dbFile.absolutePath) .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) + .addMigrations(Migration2to3(deviceIdStore::getDeviceId)) .build() }