-
Notifications
You must be signed in to change notification settings - Fork 19
Import and export data #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
760813f
c17da6d
daed80b
1d61653
e8bb260
8e2aed6
3bc0c36
a6b6f97
eb46859
f1ee9b5
70c7dc0
cab0cc9
aab175e
703cfb7
3d0f7aa
3ecf5e0
e7f57bc
0d1f0c6
7d6d1cf
3988156
2240b70
67efb57
d7000c9
29d6a42
01a3c42
edb8a62
48d9855
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package org.librefit.db.converters | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
| import org.librefit.db.entity.Exercise | ||
| import org.librefit.db.entity.ExerciseDC | ||
| import org.librefit.db.entity.Measurement | ||
| import org.librefit.db.entity.Workout | ||
| import org.librefit.db.entity.Set | ||
|
|
||
| @Serializable | ||
| data class ExportPayload( | ||
| val version: Int, | ||
| val data: ExportData | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class ExportData( | ||
| val workouts: List<Workout>, | ||
| val exercises: List<Exercise>, | ||
| val sets: List<Set>, | ||
| val measurements: List<Measurement>, | ||
| val exerciseDCs: List<ExerciseDC> | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,9 @@ interface DatasetDao { | |||||
| @Query("SELECT * FROM dataset ORDER BY name") | ||||||
| fun getDataset(): Flow<List<ExerciseDC>> | ||||||
|
|
||||||
| @Query("SELECT * FROM dataset ORDER BY name") | ||||||
| suspend fun getAllExerciseDCs(): List<ExerciseDC> | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use standard naming conventions
Suggested change
|
||||||
|
|
||||||
| @Query("SELECT * FROM dataset WHERE isCustomExercise") | ||||||
| fun getCustomExercises(): Flow<List<ExerciseDC>> | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,9 @@ interface MeasurementDao { | |||||
| @Query("SELECT * FROM measurements ORDER BY date DESC") | ||||||
| fun getAllMeasurements(): Flow<List<Measurement>> | ||||||
|
|
||||||
| @Query("SELECT * FROM measurements ORDER BY date DESC") | ||||||
| suspend fun getAllMeasurementsForBackup(): List<Measurement> | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use standard naming conventions
Suggested change
|
||||||
|
|
||||||
| @Query("SELECT * FROM measurements WHERE date <= :cutoff ORDER BY date DESC") | ||||||
| suspend fun getLastMeasurementByCutoff(cutoff: LocalDateTime): Measurement? | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,14 +12,18 @@ import androidx.annotation.FloatRange | |||||
| import androidx.annotation.IntRange | ||||||
| import androidx.room.Entity | ||||||
| import androidx.room.PrimaryKey | ||||||
| import kotlinx.serialization.Contextual | ||||||
| import kotlinx.serialization.Serializable | ||||||
| import java.time.LocalDateTime | ||||||
|
|
||||||
| @Entity(tableName = "measurements") | ||||||
| @Serializable | ||||||
| data class Measurement( | ||||||
| @PrimaryKey(autoGenerate = true) val id: Long = 0L, | ||||||
| @get:FloatRange(0.0, 300.0) val bodyWeight: Double = 0.0, | ||||||
| @get:IntRange(0, 100) val bodyFatPercentage: Int = 0, | ||||||
| @get:IntRange(0, 100) val muscleMassPercentage: Int = 0, | ||||||
| @Contextual | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use this serializer
Suggested change
|
||||||
| val date: LocalDateTime = LocalDateTime.now(), | ||||||
| val notes: String = "" | ||||||
| ) | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,137 @@ | ||||||
| package org.librefit.db.repository | ||||||
|
|
||||||
| import android.content.Context | ||||||
| import android.content.Intent | ||||||
| import android.net.Uri | ||||||
| import android.util.Log | ||||||
| import androidx.room.withTransaction | ||||||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||||||
| import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.flow.first | ||||||
| import kotlinx.coroutines.withContext | ||||||
| import kotlinx.serialization.json.Json | ||||||
| import kotlinx.serialization.modules.SerializersModule | ||||||
| import org.librefit.db.AppDatabase | ||||||
| import org.librefit.db.converters.ExportData | ||||||
| import org.librefit.db.converters.ExportPayload | ||||||
| import javax.inject.Inject | ||||||
| import org.librefit.db.entity.Exercise | ||||||
| import org.librefit.db.entity.LocalDateTimeSerializer | ||||||
| import java.time.LocalDateTime | ||||||
| import kotlin.system.exitProcess | ||||||
|
|
||||||
| class ImportExportRepository @Inject constructor( | ||||||
| private val db: AppDatabase, | ||||||
| @ApplicationContext private val context: Context | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ) { | ||||||
| suspend fun exportTo(uri: Uri) = withContext(Dispatchers.IO) { | ||||||
| // 1. note the current db migration version | ||||||
| // 2. serialize the JSON | ||||||
| val workouts = db.getWorkoutDao().getAllWorkouts() | ||||||
|
|
||||||
| val workoutIds = workouts.map { it.id } | ||||||
| val exercises = db.getWorkoutDao().getAllExercises(workoutIds) | ||||||
|
|
||||||
| val exerciseIds = exercises.map { it.id } | ||||||
| val sets = db.getWorkoutDao().getAllSets(exerciseIds) | ||||||
|
|
||||||
| val measurements = db.getMeasurementDao().getAllMeasurementsForBackup() | ||||||
|
|
||||||
| val exerciseDCs = db.getDatasetDao().getAllExerciseDCs() | ||||||
|
|
||||||
| val payload = ExportPayload( | ||||||
| version = 3, | ||||||
| data = ExportData( | ||||||
| workouts = workouts, | ||||||
| exercises = exercises, | ||||||
| sets = sets, | ||||||
| measurements = measurements, | ||||||
| exerciseDCs = exerciseDCs | ||||||
| ) | ||||||
| ) | ||||||
|
|
||||||
| val outputStream = context.contentResolver.openOutputStream(uri) | ||||||
| ?: error("Cannot open output stream for export URI") | ||||||
|
|
||||||
| outputStream.use { out -> | ||||||
| val json = Json { | ||||||
| prettyPrint = true | ||||||
| ignoreUnknownKeys = true | ||||||
| encodeDefaults = true | ||||||
| serializersModule = SerializersModule { | ||||||
| contextual(LocalDateTime::class, LocalDateTimeSerializer) | ||||||
| } | ||||||
| } | ||||||
| out.write(json.encodeToString(payload).toByteArray()) | ||||||
| out.flush() | ||||||
| } | ||||||
|
|
||||||
| outputStream.close() | ||||||
| } | ||||||
|
|
||||||
| private fun normalizeExercises(exercises: List<Exercise>): List<Exercise> { | ||||||
| return exercises | ||||||
| .groupBy { it.workoutId } | ||||||
| .flatMap { (_, group) -> | ||||||
| group | ||||||
| .sortedBy { it.id } | ||||||
| .mapIndexed { index, exercise -> | ||||||
| exercise.copy(position = index) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| suspend fun importFrom(uri: Uri) = withContext(Dispatchers.IO) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remember to inject the dispatcher
Suggested change
|
||||||
| val json = Json { ignoreUnknownKeys = true } | ||||||
|
|
||||||
| val payload = context.contentResolver.openInputStream(uri)?.use { input -> | ||||||
| context.contentResolver.takePersistableUriPermission( | ||||||
| uri, | ||||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION | ||||||
| ) | ||||||
|
|
||||||
| val text = input.bufferedReader().readText() | ||||||
| json.decodeFromString<ExportPayload>(text) | ||||||
| } ?: return@withContext | ||||||
|
|
||||||
| db.withTransaction { | ||||||
|
odweta marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| val workoutDao = db.getWorkoutDao() | ||||||
| val measurementDao = db.getMeasurementDao() | ||||||
| val datasetDao = db.getDatasetDao() | ||||||
|
|
||||||
| // 1. UPSERT WORKOUTS | ||||||
| workoutDao.upsertWorkouts(payload.data.workouts) | ||||||
|
|
||||||
| // 2. UPSERT EXERCISES | ||||||
| // normalizing because of the migration to V3 of the DB schema | ||||||
| val normalizedExercises = if (payload.version < 3) | ||||||
| normalizeExercises(payload.data.exercises) | ||||||
| else | ||||||
| payload.data.exercises | ||||||
| workoutDao.upsertExercises(normalizedExercises) | ||||||
|
|
||||||
| // 3. UPSERT SETS | ||||||
| workoutDao.upsertSets(payload.data.sets) | ||||||
|
|
||||||
| // 4. UPSERT MEASUREMENTS | ||||||
| payload.data.measurements.forEach { | ||||||
| measurementDao.upsertMeasurement(it) | ||||||
| } | ||||||
|
|
||||||
| // 5. UPSERT DATASETS | ||||||
| payload.data.exerciseDCs.forEach { | ||||||
| datasetDao.upsertExercise(it) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // restart app process cleanly | ||||||
| val intent = context.packageManager | ||||||
| .getLaunchIntentForPackage(context.packageName) | ||||||
|
|
||||||
| intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||||
|
|
||||||
| context.startActivity(intent) | ||||||
| exitProcess(0) | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package org.librefit.ui.models | ||
|
|
||
| sealed interface BackupEvent { | ||
| object BackupSuccess : BackupEvent | ||
| object RestoreSuccess : BackupEvent | ||
| data class Error(val message: String) : BackupEvent | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this is added? It is redundant because it's already included in
implementation(platform(libs.androidx.compose.bom))