mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-22 08:13:43 +02:00
Implement Automatic Database Backups
This commit is contained in:
@@ -147,6 +147,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.androidx.worker)
|
||||||
|
implementation(libs.androidx.documentfile)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
@@ -19,6 +19,7 @@
|
|||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".OpenScaleApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -54,6 +55,16 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="${applicationId}.READ_WRITE_DATA">
|
android:permission="${applicationId}.READ_WRITE_DATA">
|
||||||
</provider>
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@@ -50,44 +50,6 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a default list of measurement types available in the application,
|
|
||||||
* resolving names from string resources.
|
|
||||||
* These types are intended for insertion into the database on the first app start.
|
|
||||||
*
|
|
||||||
* @param context The context used to access string resources.
|
|
||||||
* @return A list of [MeasurementType] objects.
|
|
||||||
*/
|
|
||||||
fun getDefaultMeasurementTypes(): List<MeasurementType> {
|
|
||||||
return listOf(
|
|
||||||
MeasurementType(key = MeasurementTypeKey.WEIGHT, unit = UnitType.KG, color = 0xFF7E57C2.toInt(), icon = MeasurementTypeIcon.IC_WEIGHT, isPinned = true, isEnabled = true, isOnRightYAxis = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.BMI, unit = UnitType.NONE, color = 0xFFFFCA28.toInt(), icon = MeasurementTypeIcon.IC_BMI, isDerived = true, isPinned = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.BODY_FAT, unit = UnitType.PERCENT, color = 0xFFEF5350.toInt(), icon = MeasurementTypeIcon.IC_BODY_FAT, isPinned = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.WATER, unit = UnitType.PERCENT, color = 0xFF29B6F6.toInt(), icon = MeasurementTypeIcon.IC_WATER, isPinned = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.MUSCLE, unit = UnitType.PERCENT, color = 0xFF66BB6A.toInt(), icon = MeasurementTypeIcon.IC_MUSCLE, isPinned = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.LBM, unit = UnitType.KG, color = 0xFF4DBAC0.toInt(), icon = MeasurementTypeIcon.IC_LBM, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.BONE, unit = UnitType.KG, color = 0xFFBDBDBD.toInt(), icon = MeasurementTypeIcon.IC_BONE, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.WAIST, unit = UnitType.CM, color = 0xFF78909C.toInt(), icon = MeasurementTypeIcon.IC_WAIST, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.WHR, unit = UnitType.NONE, color = 0xFFFFA726.toInt(), icon = MeasurementTypeIcon.IC_WHR, isDerived = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.WHTR, unit = UnitType.NONE, color = 0xFFFF7043.toInt(), icon = MeasurementTypeIcon.IC_WHTR, isDerived = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.HIPS, unit = UnitType.CM, color = 0xFF5C6BC0.toInt(), icon = MeasurementTypeIcon.IC_HIPS, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.VISCERAL_FAT, unit = UnitType.NONE, color = 0xFFD84315.toInt(), icon = MeasurementTypeIcon.IC_VISCERAL_FAT, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.CHEST, unit = UnitType.CM, color = 0xFF8E24AA.toInt(), icon = MeasurementTypeIcon.IC_CHEST, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.THIGH, unit = UnitType.CM, color = 0xFFA1887F.toInt(), icon = MeasurementTypeIcon.IC_THIGH, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.BICEPS, unit = UnitType.CM, color = 0xFFEC407A.toInt(), icon = MeasurementTypeIcon.IC_BICEPS, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.NECK, unit = UnitType.CM, color = 0xFFB0BEC5.toInt(), icon = MeasurementTypeIcon.IC_NECK, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.CALIPER_1, unit = UnitType.CM, color = 0xFFFFF59D.toInt(), icon = MeasurementTypeIcon.IC_CALIPER1, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.CALIPER_2, unit = UnitType.CM, color = 0xFFFFE082.toInt(), icon = MeasurementTypeIcon.IC_CALIPER2, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.CALIPER_3, unit = UnitType.CM, color = 0xFFFFCC80.toInt(), icon = MeasurementTypeIcon.IC_CALIPER3, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.CALIPER, unit = UnitType.PERCENT, color = 0xFFFB8C00.toInt(), icon = MeasurementTypeIcon.IC_FAT_CALIPER, isDerived = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.BMR, unit = UnitType.KCAL, color = 0xFFAB47BC.toInt(), icon = MeasurementTypeIcon.IC_BMR, isDerived = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.TDEE, unit = UnitType.KCAL, color = 0xFF26A69A.toInt(), icon = MeasurementTypeIcon.IC_TDEE, isDerived = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.CALORIES, unit = UnitType.KCAL, color = 0xFF4CAF50.toInt(), icon = MeasurementTypeIcon.IC_CALORIES, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.COMMENT, inputType = InputFieldType.TEXT, unit = UnitType.NONE, color = 0xFFE0E0E0.toInt(), icon = MeasurementTypeIcon.IC_COMMENT, isPinned = true, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.DATE, inputType = InputFieldType.DATE, unit = UnitType.NONE, color = 0xFF9E9E9E.toInt(), icon = MeasurementTypeIcon.IC_DATE, isEnabled = true),
|
|
||||||
MeasurementType(key = MeasurementTypeKey.TIME, inputType = InputFieldType.TIME, unit = UnitType.NONE, color = 0xFF757575.toInt(), icon = MeasurementTypeIcon.IC_TIME, isEnabled = true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main entry point of the application.
|
* The main entry point of the application.
|
||||||
@@ -99,21 +61,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
private const val TAG = "MainActivity"
|
private const val TAG = "MainActivity"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var userSettingsRepository: UserSettingsRepository // Machen Sie es zur Property
|
private val appInstance: OpenScaleApp by lazy { application as OpenScaleApp }
|
||||||
|
private val userSettingsRepository: UserSettingsRepository by lazy { appInstance.userSettingsRepository }
|
||||||
|
private val databaseRepository: DatabaseRepository by lazy { appInstance.databaseRepository }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
userSettingsRepository = provideUserSettingsRepository(applicationContext)
|
|
||||||
|
|
||||||
// --- LogManager initializing ---
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val isFileLoggingEnabled = runCatching { userSettingsRepository.isFileLoggingEnabled.first() }
|
|
||||||
.getOrElse { false }
|
|
||||||
LogManager.init(applicationContext, isFileLoggingEnabled)
|
|
||||||
LogManager.d(TAG, "LogManager initialized. File logging enabled: $isFileLoggingEnabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Language initializing ---
|
// --- Language initializing ---
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
userSettingsRepository.appLanguageCode.collectLatest { languageCode ->
|
userSettingsRepository.appLanguageCode.collectLatest { languageCode ->
|
||||||
@@ -139,30 +93,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeAndSetContent() {
|
private fun initializeAndSetContent() {
|
||||||
val db = AppDatabase.getInstance(applicationContext)
|
LogManager.d(TAG, "Initializing and setting content.")
|
||||||
val databaseRepository = DatabaseRepository(
|
|
||||||
database = db,
|
|
||||||
userDao = db.userDao(),
|
|
||||||
measurementDao = db.measurementDao(),
|
|
||||||
measurementValueDao = db.measurementValueDao(),
|
|
||||||
measurementTypeDao = db.measurementTypeDao()
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- Measurement Types initializing ---
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val isActuallyFirstStart = userSettingsRepository.isFirstAppStart.first()
|
|
||||||
LogManager.d(TAG, "Checking for first app start. isFirstAppStart: $isActuallyFirstStart")
|
|
||||||
if (isActuallyFirstStart) {
|
|
||||||
LogManager.i(TAG, "First app start detected. Inserting default measurement types...")
|
|
||||||
val defaultTypesToInsert = getDefaultMeasurementTypes()
|
|
||||||
db.measurementTypeDao().insertAll(defaultTypesToInsert)
|
|
||||||
userSettingsRepository.setFirstAppStartCompleted(false)
|
|
||||||
LogManager.i(TAG, "Default measurement types inserted and first start marked as completed.")
|
|
||||||
} else {
|
|
||||||
LogManager.d(TAG, "Not the first app start. Default data should already exist.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Configuration
|
||||||
|
import com.health.openscale.core.data.InputFieldType
|
||||||
|
import com.health.openscale.core.data.MeasurementType
|
||||||
|
import com.health.openscale.core.data.MeasurementTypeIcon
|
||||||
|
import com.health.openscale.core.data.MeasurementTypeKey
|
||||||
|
import com.health.openscale.core.data.UnitType
|
||||||
|
import com.health.openscale.core.database.AppDatabase
|
||||||
|
import com.health.openscale.core.database.DatabaseRepository
|
||||||
|
import com.health.openscale.core.database.UserSettingsRepository
|
||||||
|
import com.health.openscale.core.database.provideUserSettingsRepository
|
||||||
|
import com.health.openscale.core.utils.LogManager
|
||||||
|
import com.health.openscale.core.worker.TaskWorkerFactory
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a default list of measurement types available in the application,
|
||||||
|
* resolving names from string resources.
|
||||||
|
* These types are intended for insertion into the database on the first app start.
|
||||||
|
*
|
||||||
|
* @param context The context used to access string resources.
|
||||||
|
* @return A list of [MeasurementType] objects.
|
||||||
|
*/
|
||||||
|
fun getDefaultMeasurementTypes(): List<MeasurementType> {
|
||||||
|
return listOf(
|
||||||
|
MeasurementType(key = MeasurementTypeKey.WEIGHT, unit = UnitType.KG, color = 0xFF7E57C2.toInt(), icon = MeasurementTypeIcon.IC_WEIGHT, isPinned = true, isEnabled = true, isOnRightYAxis = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.BMI, unit = UnitType.NONE, color = 0xFFFFCA28.toInt(), icon = MeasurementTypeIcon.IC_BMI, isDerived = true, isPinned = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.BODY_FAT, unit = UnitType.PERCENT, color = 0xFFEF5350.toInt(), icon = MeasurementTypeIcon.IC_BODY_FAT, isPinned = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.WATER, unit = UnitType.PERCENT, color = 0xFF29B6F6.toInt(), icon = MeasurementTypeIcon.IC_WATER, isPinned = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.MUSCLE, unit = UnitType.PERCENT, color = 0xFF66BB6A.toInt(), icon = MeasurementTypeIcon.IC_MUSCLE, isPinned = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.LBM, unit = UnitType.KG, color = 0xFF4DBAC0.toInt(), icon = MeasurementTypeIcon.IC_LBM, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.BONE, unit = UnitType.KG, color = 0xFFBDBDBD.toInt(), icon = MeasurementTypeIcon.IC_BONE, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.WAIST, unit = UnitType.CM, color = 0xFF78909C.toInt(), icon = MeasurementTypeIcon.IC_WAIST, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.WHR, unit = UnitType.NONE, color = 0xFFFFA726.toInt(), icon = MeasurementTypeIcon.IC_WHR, isDerived = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.WHTR, unit = UnitType.NONE, color = 0xFFFF7043.toInt(), icon = MeasurementTypeIcon.IC_WHTR, isDerived = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.HIPS, unit = UnitType.CM, color = 0xFF5C6BC0.toInt(), icon = MeasurementTypeIcon.IC_HIPS, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.VISCERAL_FAT, unit = UnitType.NONE, color = 0xFFD84315.toInt(), icon = MeasurementTypeIcon.IC_VISCERAL_FAT, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.CHEST, unit = UnitType.CM, color = 0xFF8E24AA.toInt(), icon = MeasurementTypeIcon.IC_CHEST, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.THIGH, unit = UnitType.CM, color = 0xFFA1887F.toInt(), icon = MeasurementTypeIcon.IC_THIGH, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.BICEPS, unit = UnitType.CM, color = 0xFFEC407A.toInt(), icon = MeasurementTypeIcon.IC_BICEPS, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.NECK, unit = UnitType.CM, color = 0xFFB0BEC5.toInt(), icon = MeasurementTypeIcon.IC_NECK, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.CALIPER_1, unit = UnitType.CM, color = 0xFFFFF59D.toInt(), icon = MeasurementTypeIcon.IC_CALIPER1, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.CALIPER_2, unit = UnitType.CM, color = 0xFFFFE082.toInt(), icon = MeasurementTypeIcon.IC_CALIPER2, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.CALIPER_3, unit = UnitType.CM, color = 0xFFFFCC80.toInt(), icon = MeasurementTypeIcon.IC_CALIPER3, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.CALIPER, unit = UnitType.PERCENT, color = 0xFFFB8C00.toInt(), icon = MeasurementTypeIcon.IC_FAT_CALIPER, isDerived = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.BMR, unit = UnitType.KCAL, color = 0xFFAB47BC.toInt(), icon = MeasurementTypeIcon.IC_BMR, isDerived = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.TDEE, unit = UnitType.KCAL, color = 0xFF26A69A.toInt(), icon = MeasurementTypeIcon.IC_TDEE, isDerived = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.CALORIES, unit = UnitType.KCAL, color = 0xFF4CAF50.toInt(), icon = MeasurementTypeIcon.IC_CALORIES, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.COMMENT, inputType = InputFieldType.TEXT, unit = UnitType.NONE, color = 0xFFE0E0E0.toInt(), icon = MeasurementTypeIcon.IC_COMMENT, isPinned = true, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.DATE, inputType = InputFieldType.DATE, unit = UnitType.NONE, color = 0xFF9E9E9E.toInt(), icon = MeasurementTypeIcon.IC_DATE, isEnabled = true),
|
||||||
|
MeasurementType(key = MeasurementTypeKey.TIME, inputType = InputFieldType.TIME, unit = UnitType.NONE, color = 0xFF757575.toInt(), icon = MeasurementTypeIcon.IC_TIME, isEnabled = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenScaleApp : Application(), Configuration.Provider {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "OpenScaleApp"
|
||||||
|
}
|
||||||
|
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
val userSettingsRepository: UserSettingsRepository by lazy {
|
||||||
|
provideUserSettingsRepository(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
val databaseRepository: DatabaseRepository by lazy {
|
||||||
|
val db = AppDatabase.getInstance(applicationContext)
|
||||||
|
DatabaseRepository(
|
||||||
|
database = db,
|
||||||
|
userDao = db.userDao(),
|
||||||
|
measurementDao = db.measurementDao(),
|
||||||
|
measurementValueDao = db.measurementValueDao(),
|
||||||
|
measurementTypeDao = db.measurementTypeDao()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
initializeLogging()
|
||||||
|
initializeDefaultData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeLogging() {
|
||||||
|
applicationScope.launch {
|
||||||
|
val isFileLoggingEnabled = try {
|
||||||
|
userSettingsRepository.isFileLoggingEnabled.first()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Log to standard Android Log if our LogManager or DataStore fails early
|
||||||
|
Log.e(TAG, "Failed to retrieve isFileLoggingEnabled setting", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
LogManager.init(applicationContext, isFileLoggingEnabled)
|
||||||
|
LogManager.i(TAG, "LogManager initialized. File logging enabled: $isFileLoggingEnabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeDefaultData() {
|
||||||
|
applicationScope.launch(Dispatchers.IO) { // Use IO dispatcher for database operations
|
||||||
|
try {
|
||||||
|
val isFirstActualStart = userSettingsRepository.isFirstAppStart.first()
|
||||||
|
LogManager.d(TAG, "Checking for first app start. isFirstAppStart: $isFirstActualStart")
|
||||||
|
|
||||||
|
if (isFirstActualStart) {
|
||||||
|
LogManager.i(TAG, "First app start detected. Inserting default measurement types...")
|
||||||
|
databaseRepository.insertAllMeasurementTypes(getDefaultMeasurementTypes())
|
||||||
|
userSettingsRepository.setFirstAppStartCompleted(false)
|
||||||
|
LogManager.i(TAG, "Default measurement types inserted and first start marked as completed.")
|
||||||
|
} else {
|
||||||
|
LogManager.d(TAG, "Not the first app start. Default data should already exist.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogManager.e(TAG, "Error during first-start data initialization", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration by lazy {
|
||||||
|
val factory = TaskWorkerFactory(
|
||||||
|
userSettingsRepository = userSettingsRepository,
|
||||||
|
databaseRepository = databaseRepository
|
||||||
|
)
|
||||||
|
Configuration.Builder()
|
||||||
|
.setWorkerFactory(factory)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.health.openscale.core.data
|
package com.health.openscale.core.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -350,3 +351,17 @@ enum class SmoothingAlgorithm(@StringRes val displayNameResId: Int) {
|
|||||||
return context.getString(displayNameResId)
|
return context.getString(displayNameResId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class BackupInterval {
|
||||||
|
DAILY,
|
||||||
|
WEEKLY,
|
||||||
|
MONTHLY;
|
||||||
|
|
||||||
|
fun getDisplayName(context: Context): String {
|
||||||
|
return when (this) {
|
||||||
|
DAILY -> context.getString(R.string.interval_daily)
|
||||||
|
WEEKLY -> context.getString(R.string.interval_weekly)
|
||||||
|
MONTHLY -> context.getString(R.string.interval_monthly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -188,6 +188,10 @@ class DatabaseRepository(
|
|||||||
|
|
||||||
// --- Measurement Type Operations ---
|
// --- Measurement Type Operations ---
|
||||||
|
|
||||||
|
suspend fun insertAllMeasurementTypes(types: List<MeasurementType>) {
|
||||||
|
measurementTypeDao.insertAll(types)
|
||||||
|
}
|
||||||
|
|
||||||
fun getAllMeasurementTypes(): Flow<List<MeasurementType>> = measurementTypeDao.getAll()
|
fun getAllMeasurementTypes(): Flow<List<MeasurementType>> = measurementTypeDao.getAll()
|
||||||
|
|
||||||
suspend fun insertMeasurementType(type: MeasurementType): Long {
|
suspend fun insertMeasurementType(type: MeasurementType): Long {
|
||||||
|
@@ -30,6 +30,7 @@ import androidx.datastore.preferences.core.longPreferencesKey
|
|||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.health.openscale.core.data.BackupInterval
|
||||||
import com.health.openscale.core.data.SmoothingAlgorithm
|
import com.health.openscale.core.data.SmoothingAlgorithm
|
||||||
import com.health.openscale.core.utils.LogManager
|
import com.health.openscale.core.utils.LogManager
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -64,6 +65,13 @@ object UserPreferenceKeys {
|
|||||||
val CHART_SMOOTHING_ALPHA = floatPreferencesKey("chart_smoothing_alpha")
|
val CHART_SMOOTHING_ALPHA = floatPreferencesKey("chart_smoothing_alpha")
|
||||||
val CHART_SMOOTHING_WINDOW_SIZE = intPreferencesKey("chart_smoothing_window_size")
|
val CHART_SMOOTHING_WINDOW_SIZE = intPreferencesKey("chart_smoothing_window_size")
|
||||||
|
|
||||||
|
// --- Settings for Automatic Backups ---
|
||||||
|
val AUTO_BACKUP_ENABLED_GLOBALLY = booleanPreferencesKey("auto_backup_enabled_globally")
|
||||||
|
val AUTO_BACKUP_LOCATION_URI = stringPreferencesKey("auto_backup_location_uri")
|
||||||
|
val AUTO_BACKUP_INTERVAL = stringPreferencesKey("auto_backup_interval")
|
||||||
|
val AUTO_BACKUP_CREATE_NEW_FILE = booleanPreferencesKey("auto_backup_create_new_file")
|
||||||
|
val AUTO_BACKUP_LAST_SUCCESSFUL_TIMESTAMP = longPreferencesKey("auto_backup_last_successful_timestamp")
|
||||||
|
|
||||||
// Context strings for screen-specific settings (can be used as prefixes for dynamic keys)
|
// Context strings for screen-specific settings (can be used as prefixes for dynamic keys)
|
||||||
const val OVERVIEW_SCREEN_CONTEXT = "overview_screen"
|
const val OVERVIEW_SCREEN_CONTEXT = "overview_screen"
|
||||||
const val GRAPH_SCREEN_CONTEXT = "graph_screen"
|
const val GRAPH_SCREEN_CONTEXT = "graph_screen"
|
||||||
@@ -109,6 +117,22 @@ interface UserSettingsRepository {
|
|||||||
val chartSmoothingWindowSize: Flow<Int>
|
val chartSmoothingWindowSize: Flow<Int>
|
||||||
suspend fun setChartSmoothingWindowSize(windowSize: Int)
|
suspend fun setChartSmoothingWindowSize(windowSize: Int)
|
||||||
|
|
||||||
|
// --- Automatic Backup Settings ---
|
||||||
|
val autoBackupEnabledGlobally: Flow<Boolean>
|
||||||
|
suspend fun setAutoBackupEnabledGlobally(enabled: Boolean)
|
||||||
|
|
||||||
|
val autoBackupLocationUri: Flow<String?>
|
||||||
|
suspend fun setAutoBackupLocationUri(uri: String?)
|
||||||
|
|
||||||
|
val autoBackupInterval: Flow<BackupInterval>
|
||||||
|
suspend fun setAutoBackupInterval(interval: BackupInterval)
|
||||||
|
|
||||||
|
val autoBackupCreateNewFile: Flow<Boolean>
|
||||||
|
suspend fun setAutoBackupCreateNewFile(createNew: Boolean)
|
||||||
|
|
||||||
|
val autoBackupLastSuccessfulTimestamp: Flow<Long>
|
||||||
|
suspend fun setAutoBackupLastSuccessfulTimestamp(timestamp: Long)
|
||||||
|
|
||||||
// Generic Settings Accessors
|
// Generic Settings Accessors
|
||||||
/**
|
/**
|
||||||
* Observes a setting with the given key name and default value.
|
* Observes a setting with the given key name and default value.
|
||||||
@@ -333,6 +357,96 @@ class UserSettingsRepositoryImpl(context: Context) : UserSettingsRepository {
|
|||||||
saveSetting(UserPreferenceKeys.CHART_SMOOTHING_WINDOW_SIZE.name, validWindowSize)
|
saveSetting(UserPreferenceKeys.CHART_SMOOTHING_WINDOW_SIZE.name, validWindowSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val autoBackupEnabledGlobally: Flow<Boolean> = observeSetting(
|
||||||
|
UserPreferenceKeys.AUTO_BACKUP_ENABLED_GLOBALLY.name,
|
||||||
|
false // Standardmäßig deaktiviert
|
||||||
|
).catch { exception ->
|
||||||
|
LogManager.e(TAG, "Error observing autoBackupEnabledGlobally", exception)
|
||||||
|
emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setAutoBackupEnabledGlobally(enabled: Boolean) {
|
||||||
|
LogManager.d(TAG, "Setting autoBackupEnabledGlobally to: $enabled")
|
||||||
|
saveSetting(UserPreferenceKeys.AUTO_BACKUP_ENABLED_GLOBALLY.name, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val autoBackupLocationUri: Flow<String?> = dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
LogManager.e(TAG, "Error reading autoBackupLocationUri from DataStore.", exception)
|
||||||
|
if (exception is IOException) {
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { preferences ->
|
||||||
|
preferences[UserPreferenceKeys.AUTO_BACKUP_LOCATION_URI]
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
|
override suspend fun setAutoBackupLocationUri(uri: String?) {
|
||||||
|
LogManager.d(TAG, "Setting autoBackupLocationUri to: $uri")
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
if (uri != null) {
|
||||||
|
preferences[UserPreferenceKeys.AUTO_BACKUP_LOCATION_URI] = uri
|
||||||
|
} else {
|
||||||
|
preferences.remove(UserPreferenceKeys.AUTO_BACKUP_LOCATION_URI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val autoBackupInterval: Flow<BackupInterval> = dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
LogManager.e(TAG, "Error reading autoBackupInterval from DataStore.", exception)
|
||||||
|
if (exception is IOException) {
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { preferences ->
|
||||||
|
val intervalName = preferences[UserPreferenceKeys.AUTO_BACKUP_INTERVAL]
|
||||||
|
try {
|
||||||
|
intervalName?.let { BackupInterval.valueOf(it) } ?: BackupInterval.WEEKLY
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
LogManager.w(TAG, "Invalid BackupInterval name '$intervalName' in DataStore. Defaulting to WEEKLY.", e)
|
||||||
|
BackupInterval.WEEKLY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
|
override suspend fun setAutoBackupInterval(interval: BackupInterval) {
|
||||||
|
LogManager.d(TAG, "Setting autoBackupInterval to: ${interval.name}")
|
||||||
|
saveSetting(UserPreferenceKeys.AUTO_BACKUP_INTERVAL.name, interval.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override val autoBackupCreateNewFile: Flow<Boolean> = observeSetting(
|
||||||
|
UserPreferenceKeys.AUTO_BACKUP_CREATE_NEW_FILE.name,
|
||||||
|
false
|
||||||
|
).catch { exception ->
|
||||||
|
LogManager.e(TAG, "Error observing autoBackupCreateNewFile", exception)
|
||||||
|
emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setAutoBackupCreateNewFile(createNew: Boolean) {
|
||||||
|
LogManager.d(TAG, "Setting autoBackupCreateNewFile to: $createNew")
|
||||||
|
saveSetting(UserPreferenceKeys.AUTO_BACKUP_CREATE_NEW_FILE.name, createNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val autoBackupLastSuccessfulTimestamp: Flow<Long> = observeSetting(
|
||||||
|
UserPreferenceKeys.AUTO_BACKUP_LAST_SUCCESSFUL_TIMESTAMP.name,
|
||||||
|
0L
|
||||||
|
).catch { exception ->
|
||||||
|
LogManager.e(TAG, "Error observing autoBackupLastSuccessfulTimestamp", exception)
|
||||||
|
emit(0L) // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setAutoBackupLastSuccessfulTimestamp(timestamp: Long) {
|
||||||
|
LogManager.d(TAG, "Setting autoBackupLastSuccessfulTimestamp to: $timestamp")
|
||||||
|
saveSetting(UserPreferenceKeys.AUTO_BACKUP_LAST_SUCCESSFUL_TIMESTAMP.name, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T> observeSetting(keyName: String, defaultValue: T): Flow<T> {
|
override fun <T> observeSetting(keyName: String, defaultValue: T): Flow<T> {
|
||||||
LogManager.v(TAG, "Observing setting: key='$keyName', type='${defaultValue!!::class.simpleName}'")
|
LogManager.v(TAG, "Observing setting: key='$keyName', type='${defaultValue!!::class.simpleName}'")
|
||||||
|
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.worker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.health.openscale.core.database.DatabaseRepository
|
||||||
|
import com.health.openscale.core.database.UserSettingsRepository
|
||||||
|
import com.health.openscale.core.utils.LogManager
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
|
||||||
|
class BackupWorker(
|
||||||
|
appContext: Context,
|
||||||
|
workerParams: WorkerParameters,
|
||||||
|
private val userSettingsRepository: UserSettingsRepository,
|
||||||
|
private val databaseRepository: DatabaseRepository
|
||||||
|
) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "AutoBackupWorker"
|
||||||
|
const val WORK_NAME = "com.health.openscale.AUTO_DATABASE_BACKUP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
LogManager.i(TAG, "Automatic backup worker started.")
|
||||||
|
|
||||||
|
val isEnabled = userSettingsRepository.autoBackupEnabledGlobally.first()
|
||||||
|
val locationUriString = userSettingsRepository.autoBackupLocationUri.first()
|
||||||
|
val createNewFile = userSettingsRepository.autoBackupCreateNewFile.first()
|
||||||
|
|
||||||
|
if (!isEnabled || locationUriString == null) {
|
||||||
|
LogManager.i(TAG, "Auto backup is disabled or location not set. Worker finishing.")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupDirUri = Uri.parse(locationUriString)
|
||||||
|
val parentDocumentFile = DocumentFile.fromTreeUri(applicationContext, backupDirUri)
|
||||||
|
|
||||||
|
if (parentDocumentFile == null || !parentDocumentFile.canWrite()) {
|
||||||
|
LogManager.e(TAG, "Cannot write to backup location: $locationUriString. Permissions might be lost or URI invalid.")
|
||||||
|
userSettingsRepository.setAutoBackupLastSuccessfulTimestamp(0L)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dbName = databaseRepository.getDatabaseName()
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val baseBackupFileName = "${dbName}_auto_backup"
|
||||||
|
val finalFileName = if (createNewFile) "${baseBackupFileName}_$timeStamp.zip" else "$baseBackupFileName.zip"
|
||||||
|
|
||||||
|
var backupDocumentFile = parentDocumentFile.findFile(finalFileName)
|
||||||
|
if (backupDocumentFile != null && backupDocumentFile.exists()) {
|
||||||
|
if (createNewFile) {
|
||||||
|
LogManager.w(TAG, "File $finalFileName already exists, but createNewFile is true. Creating with new timestamp again.")
|
||||||
|
} else {
|
||||||
|
if (!backupDocumentFile.delete()) {
|
||||||
|
LogManager.e(TAG, "Could not delete existing file $finalFileName for overwrite.")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
backupDocumentFile = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupDocumentFile == null) {
|
||||||
|
backupDocumentFile = parentDocumentFile.createFile("application/zip", finalFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupDocumentFile == null) {
|
||||||
|
LogManager.e(TAG, "Could not create backup file: $finalFileName in $locationUriString")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationContext.contentResolver.openOutputStream(backupDocumentFile.uri)?.use { outputStream ->
|
||||||
|
ZipOutputStream(outputStream).use { zipOutputStream ->
|
||||||
|
val dbFile = applicationContext.getDatabasePath(dbName)
|
||||||
|
val dbDir = dbFile.parentFile ?: return Result.failure()
|
||||||
|
|
||||||
|
val filesToBackup = listOfNotNull(
|
||||||
|
dbFile,
|
||||||
|
File(dbDir, "$dbName-shm"),
|
||||||
|
File(dbDir, "$dbName-wal")
|
||||||
|
)
|
||||||
|
|
||||||
|
filesToBackup.forEach { file ->
|
||||||
|
if (file.exists() && file.isFile) {
|
||||||
|
FileInputStream(file).use { fileInputStream ->
|
||||||
|
val entry = ZipEntry(file.name)
|
||||||
|
zipOutputStream.putNextEntry(entry)
|
||||||
|
fileInputStream.copyTo(zipOutputStream)
|
||||||
|
zipOutputStream.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: return Result.failure()
|
||||||
|
|
||||||
|
LogManager.i(TAG, "Automatic backup successful to: ${backupDocumentFile.uri}")
|
||||||
|
userSettingsRepository.setAutoBackupLastSuccessfulTimestamp(System.currentTimeMillis())
|
||||||
|
return Result.success()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogManager.e(TAG, "Error during automatic backup", e)
|
||||||
|
userSettingsRepository.setAutoBackupLastSuccessfulTimestamp(0L)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.worker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.WorkerFactory
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.health.openscale.core.database.DatabaseRepository
|
||||||
|
import com.health.openscale.core.database.UserSettingsRepository
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom [WorkerFactory] for creating worker instances with injected dependencies.
|
||||||
|
*
|
||||||
|
* This factory is provided to WorkManager during its initialization (via [androidx.work.Configuration.Builder.setWorkerFactory])
|
||||||
|
* to allow construction of workers that require dependencies beyond the default `Context` and `WorkerParameters`.
|
||||||
|
*/
|
||||||
|
class TaskWorkerFactory(
|
||||||
|
// Dependencies provided by the Application class, to be passed to the workers.
|
||||||
|
private val userSettingsRepository: UserSettingsRepository,
|
||||||
|
private val databaseRepository: DatabaseRepository
|
||||||
|
) : WorkerFactory() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by WorkManager to create an instance of a [ListenableWorker].
|
||||||
|
*
|
||||||
|
* @param appContext The application [Context].
|
||||||
|
* @param workerClassName The fully-qualified class name of the worker to create.
|
||||||
|
* @param workerParameters The [WorkerParameters] for the worker.
|
||||||
|
* @return An instance of the [ListenableWorker], or `null` if this factory
|
||||||
|
* cannot create the requested worker type.
|
||||||
|
*/
|
||||||
|
override fun createWorker(
|
||||||
|
appContext: Context,
|
||||||
|
workerClassName: String,
|
||||||
|
workerParameters: WorkerParameters
|
||||||
|
): ListenableWorker? {
|
||||||
|
return when (workerClassName) {
|
||||||
|
BackupWorker::class.java.name ->
|
||||||
|
BackupWorker(
|
||||||
|
appContext,
|
||||||
|
workerParameters,
|
||||||
|
userSettingsRepository,
|
||||||
|
databaseRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add other 'when' branches here if you have other custom workers
|
||||||
|
// that this factory should create.
|
||||||
|
// e.g.:
|
||||||
|
// AnotherCustomWorker::class.java.name ->
|
||||||
|
// AnotherCustomWorker(appContext, workerParameters, someOtherDependency)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -17,14 +17,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.health.openscale.ui.screen.settings
|
package com.health.openscale.ui.screen.settings
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.application
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
|
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
|
||||||
import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter
|
import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter
|
||||||
import com.health.openscale.R
|
import com.health.openscale.R
|
||||||
|
import com.health.openscale.core.data.BackupInterval
|
||||||
import com.health.openscale.core.data.InputFieldType
|
import com.health.openscale.core.data.InputFieldType
|
||||||
import com.health.openscale.core.data.Measurement
|
import com.health.openscale.core.data.Measurement
|
||||||
import com.health.openscale.core.data.MeasurementType
|
import com.health.openscale.core.data.MeasurementType
|
||||||
@@ -37,16 +46,19 @@ import com.health.openscale.core.model.MeasurementWithValues
|
|||||||
import com.health.openscale.core.utils.CalculationUtil
|
import com.health.openscale.core.utils.CalculationUtil
|
||||||
import com.health.openscale.core.utils.Converters
|
import com.health.openscale.core.utils.Converters
|
||||||
import com.health.openscale.core.utils.LogManager
|
import com.health.openscale.core.utils.LogManager
|
||||||
|
import com.health.openscale.core.worker.BackupWorker
|
||||||
import com.health.openscale.ui.screen.SharedViewModel
|
import com.health.openscale.ui.screen.SharedViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -66,6 +78,7 @@ import java.time.temporal.ChronoField
|
|||||||
import java.time.temporal.TemporalQueries.localDate
|
import java.time.temporal.TemporalQueries.localDate
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@@ -129,6 +142,22 @@ class SettingsViewModel(
|
|||||||
private val _isLoadingEntireDatabaseDeletion = MutableStateFlow(false)
|
private val _isLoadingEntireDatabaseDeletion = MutableStateFlow(false)
|
||||||
val isLoadingEntireDatabaseDeletion: StateFlow<Boolean> = _isLoadingEntireDatabaseDeletion.asStateFlow()
|
val isLoadingEntireDatabaseDeletion: StateFlow<Boolean> = _isLoadingEntireDatabaseDeletion.asStateFlow()
|
||||||
|
|
||||||
|
val autoBackupEnabledGlobally: StateFlow<Boolean> = userSettingsRepository.autoBackupEnabledGlobally
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
|
val autoBackupLocationUri: StateFlow<String?> = userSettingsRepository.autoBackupLocationUri
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
|
||||||
|
val autoBackupInterval: StateFlow<BackupInterval> = userSettingsRepository.autoBackupInterval
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BackupInterval.WEEKLY)
|
||||||
|
|
||||||
|
val autoBackupCreateNewFile: StateFlow<Boolean> = userSettingsRepository.autoBackupCreateNewFile
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
|
||||||
|
|
||||||
|
val autoBackupLastSuccessfulTimestamp: StateFlow<Long> = userSettingsRepository.autoBackupLastSuccessfulTimestamp
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L)
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SettingsViewModel"
|
private const val TAG = "SettingsViewModel"
|
||||||
const val ACTION_ID_EXPORT_USER_DATA = "export_user_data"
|
const val ACTION_ID_EXPORT_USER_DATA = "export_user_data"
|
||||||
@@ -209,6 +238,76 @@ class SettingsViewModel(
|
|||||||
return defaultLang
|
return defaultLang
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setAutoBackupEnabledGlobally(context : Context, enabled: Boolean) {
|
||||||
|
LogManager.d(TAG, "Setting auto backup globally enabled to: $enabled")
|
||||||
|
userSettingsRepository.setAutoBackupEnabledGlobally(enabled)
|
||||||
|
scheduleOrCancelAutoBackupWorker(context.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setAutoBackupLocationUri(context : Context, uriString: String?) {
|
||||||
|
LogManager.d(TAG, "Setting auto backup location URI to: $uriString")
|
||||||
|
userSettingsRepository.setAutoBackupLocationUri(uriString)
|
||||||
|
if (uriString != null && !autoBackupEnabledGlobally.value) {
|
||||||
|
setAutoBackupEnabledGlobally(context, true)
|
||||||
|
} else if (uriString == null) {
|
||||||
|
setAutoBackupEnabledGlobally(context,false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setAutoBackupInterval(context : Context, interval: BackupInterval) {
|
||||||
|
LogManager.d(TAG, "Setting auto backup interval to: ${interval.name}")
|
||||||
|
userSettingsRepository.setAutoBackupInterval(interval)
|
||||||
|
scheduleOrCancelAutoBackupWorker(context.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setAutoBackupCreateNewFile(createNew: Boolean) {
|
||||||
|
LogManager.d(TAG, "Setting auto backup create new file to: $createNew")
|
||||||
|
userSettingsRepository.setAutoBackupCreateNewFile(createNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleOrCancelAutoBackupWorker(context: Context) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val isEnabled = userSettingsRepository.autoBackupEnabledGlobally.first()
|
||||||
|
val intervalSetting = userSettingsRepository.autoBackupInterval.first()
|
||||||
|
val locationUri = userSettingsRepository.autoBackupLocationUri.first()
|
||||||
|
|
||||||
|
val workManager = WorkManager.getInstance(context.applicationContext)
|
||||||
|
|
||||||
|
if (isEnabled && locationUri != null) {
|
||||||
|
val repeatIntervalMillis = when (intervalSetting) {
|
||||||
|
BackupInterval.DAILY -> 24 * 60 * 60 * 1000L
|
||||||
|
BackupInterval.WEEKLY -> 7 * 24 * 60 * 60 * 1000L
|
||||||
|
BackupInterval.MONTHLY -> 30 * 24 * 60 * 60 * 1000L
|
||||||
|
}
|
||||||
|
|
||||||
|
val flexMillis = repeatIntervalMillis / 8
|
||||||
|
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
|
||||||
|
.setRequiresStorageNotLow(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val backupWorkRequest = PeriodicWorkRequestBuilder<BackupWorker>(
|
||||||
|
repeatIntervalMillis, TimeUnit.MILLISECONDS,
|
||||||
|
flexMillis, TimeUnit.MILLISECONDS
|
||||||
|
)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.addTag(BackupWorker.TAG)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
BackupWorker.WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.REPLACE,
|
||||||
|
backupWorkRequest
|
||||||
|
)
|
||||||
|
LogManager.i(TAG, "Scheduled auto backup worker with interval: $intervalSetting")
|
||||||
|
} else {
|
||||||
|
workManager.cancelUniqueWork(BackupWorker.WORK_NAME)
|
||||||
|
LogManager.i(TAG, "Cancelled auto backup worker.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun performCsvExport(userId: Int, uri: Uri, contentResolver: ContentResolver) {
|
fun performCsvExport(userId: Int, uri: Uri, contentResolver: ContentResolver) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoadingExport.value = true
|
_isLoadingExport.value = true
|
||||||
@@ -865,7 +964,7 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performDatabaseRestore(restoreUri: Uri, applicationContext: android.content.Context, contentResolver: ContentResolver) {
|
fun performDatabaseRestore(restoreUri: Uri, applicationContext: Context, contentResolver: ContentResolver) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoadingRestore.value = true
|
_isLoadingRestore.value = true
|
||||||
LogManager.i(TAG, "Performing database restore from URI: $restoreUri")
|
LogManager.i(TAG, "Performing database restore from URI: $restoreUri")
|
||||||
@@ -988,7 +1087,7 @@ class SettingsViewModel(
|
|||||||
LogManager.d(TAG, "Delete entire database confirmation cancelled.")
|
LogManager.d(TAG, "Delete entire database confirmation cancelled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun confirmDeleteEntireDatabase(applicationContext: android.content.Context) {
|
fun confirmDeleteEntireDatabase(applicationContext: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoadingEntireDatabaseDeletion.value = true
|
_isLoadingEntireDatabaseDeletion.value = true
|
||||||
_showDeleteEntireDatabaseConfirmationDialog.value = false
|
_showDeleteEntireDatabaseConfirmationDialog.value = false
|
||||||
|
@@ -331,6 +331,44 @@
|
|||||||
<string name="settings_delete_entire_database">Gesamte Datenbank löschen</string>
|
<string name="settings_delete_entire_database">Gesamte Datenbank löschen</string>
|
||||||
<string name="settings_danger_zone">Gefahrenzone</string>
|
<string name="settings_danger_zone">Gefahrenzone</string>
|
||||||
<string name="settings_unknown_error">Unbekannter Fehler</string>
|
<string name="settings_unknown_error">Unbekannter Fehler</string>
|
||||||
|
<string name="settings_auto_backup_title">Automatische Sicherungen</string>
|
||||||
|
<string name="settings_enable_auto_backups">Automatische Sicherungen aktivieren</string>
|
||||||
|
<string name="content_desc_auto_backups_toggle">Automatische Sicherungen umschalten</string>
|
||||||
|
<string name="settings_last_backup_status_label">Status der letzten Sicherung</string>
|
||||||
|
<string name="content_desc_backup_status_icon">Informationen zum Sicherungsstatus</string>
|
||||||
|
<string name="settings_backup_location_label">Sicherungsort</string>
|
||||||
|
<string name="content_desc_backup_location_icon">Einstellung Sicherungsort</string>
|
||||||
|
<string name="content_desc_open_backup_location_icon">Sicherungsort öffnen</string>
|
||||||
|
<string name="content_desc_change_backup_location_icon">Sicherungsort ändern</string>
|
||||||
|
<string name="settings_backup_interval_label">Sicherungsintervall</string>
|
||||||
|
<string name="content_desc_backup_interval_icon">Einstellung Sicherungsintervall</string>
|
||||||
|
<string name="content_desc_change_interval_icon">Sicherungsintervall ändern</string>
|
||||||
|
<string name="settings_backup_behavior_label">Sicherungsverhalten</string>
|
||||||
|
<string name="content_desc_backup_behavior_icon">Einstellung Sicherungsverhalten</string>
|
||||||
|
<string name="settings_last_backup_status_placeholder">Letzte Sicherung: %1$s</string>
|
||||||
|
<string name="settings_auto_backups_disabled">Automatische Sicherungen deaktiviert</string>
|
||||||
|
<string name="settings_backup_behavior_new_file">Immer neue Sicherungsdatei erstellen</string>
|
||||||
|
<string name="settings_backup_behavior_overwrite">Vorhandene Sicherungsdatei überschreiben</string>
|
||||||
|
<string name="settings_backup_location_default">Standard: App-spezifischer Ordner</string>
|
||||||
|
<string name="settings_backup_location_simulated_user_choice">Simuliert: Benutzer hat einen Ordner ausgewählt</string>
|
||||||
|
<string name="dialog_title_select_backup_interval">Sicherungsintervall auswählen</string>
|
||||||
|
<string name="settings_backup_location_not_configured">Sicherungsort nicht konfiguriert</string>
|
||||||
|
<string name="settings_backup_location_not_configured_for_auto">Ort nicht konfiguriert. Automatische Sicherungen pausiert.</string>
|
||||||
|
<string name="settings_backup_location_error_accessing">Fehler beim Zugriff auf Sicherungsort</string>
|
||||||
|
<string name="settings_backup_database_manual">Datenbank sichern (Manuell)</string>
|
||||||
|
<string name="content_desc_delete_icon">Löschen-Symbol</string>
|
||||||
|
<string name="dialog_title_select_backup_directory">Sicherungsverzeichnis auswählen</string>
|
||||||
|
<string name="settings_backup_location_select_action">Sicherungsort auswählen</string>
|
||||||
|
<string name="settings_backup_location_selected_folder">Ausgewählter Ordner</string>
|
||||||
|
<string name="settings_backup_location_open_error_no_app">Keine Anwendung zum Öffnen des Ordners gefunden.</string>
|
||||||
|
<string name="settings_backup_location_open_error">Sicherungsort konnte nicht geöffnet werden.</string>
|
||||||
|
<string name="settings_backup_location_selected_toast">Sicherungsort festgelegt auf: %1$s</string>
|
||||||
|
<string name="settings_backup_location_selection_cancelled">Ordnerauswahl abgebrochen. Automatisches Backup nicht aktiviert.</string>
|
||||||
|
<string name="settings_last_backup_status_successful">Letztes Backup: %1$s</string>
|
||||||
|
<string name="settings_last_backup_status_never">Letztes Backup: Nie</string>
|
||||||
|
<string name="interval_daily">Täglich</string>
|
||||||
|
<string name="interval_weekly">Wöchentlich</string>
|
||||||
|
<string name="interval_monthly">Monatlich</string>
|
||||||
|
|
||||||
<!-- Datenverwaltung Dialogtitel -->
|
<!-- Datenverwaltung Dialogtitel -->
|
||||||
<string name="dialog_title_export_select_user">Export: Benutzer auswählen</string>
|
<string name="dialog_title_export_select_user">Export: Benutzer auswählen</string>
|
||||||
|
@@ -333,6 +333,44 @@
|
|||||||
<string name="settings_delete_entire_database">Delete entire database</string>
|
<string name="settings_delete_entire_database">Delete entire database</string>
|
||||||
<string name="settings_danger_zone">Danger Zone</string>
|
<string name="settings_danger_zone">Danger Zone</string>
|
||||||
<string name="settings_unknown_error">Unknown error</string>
|
<string name="settings_unknown_error">Unknown error</string>
|
||||||
|
<string name="settings_auto_backup_title">Automatic Backups</string>
|
||||||
|
<string name="settings_enable_auto_backups">Enable automatic backups</string>
|
||||||
|
<string name="content_desc_auto_backups_toggle">Toggle automatic backups</string>
|
||||||
|
<string name="settings_last_backup_status_label">Last backup status</string>
|
||||||
|
<string name="content_desc_backup_status_icon">Backup status information</string>
|
||||||
|
<string name="settings_backup_location_label">Backup location</string>
|
||||||
|
<string name="content_desc_backup_location_icon">Backup location setting</string>
|
||||||
|
<string name="content_desc_open_backup_location_icon">Open backup location</string>
|
||||||
|
<string name="content_desc_change_backup_location_icon">Change backup location</string>
|
||||||
|
<string name="settings_backup_interval_label">Backup interval</string>
|
||||||
|
<string name="content_desc_backup_interval_icon">Backup interval setting</string>
|
||||||
|
<string name="content_desc_change_interval_icon">Change backup interval</string>
|
||||||
|
<string name="settings_backup_behavior_label">Backup Behavior</string>
|
||||||
|
<string name="content_desc_backup_behavior_icon">Backup behavior setting</string>
|
||||||
|
<string name="settings_last_backup_status_placeholder">Last backup: %1$s</string>
|
||||||
|
<string name="settings_auto_backups_disabled">Automatic backups disabled</string>
|
||||||
|
<string name="settings_backup_behavior_new_file">Always create a new backup file</string>
|
||||||
|
<string name="settings_backup_behavior_overwrite">Overwrite existing backup file</string>
|
||||||
|
<string name="settings_backup_location_default">Default: App-specific folder</string>
|
||||||
|
<string name="settings_backup_location_simulated_user_choice">Simulated: User chose a folder</string>
|
||||||
|
<string name="dialog_title_select_backup_interval">Select Backup Interval</string>
|
||||||
|
<string name="settings_backup_location_not_configured">Backup location not configured</string>
|
||||||
|
<string name="settings_backup_location_not_configured_for_auto">Location not configured. Automatic backups paused.</string>
|
||||||
|
<string name="settings_backup_location_error_accessing">Error accessing backup location</string>
|
||||||
|
<string name="settings_backup_database_manual">Backup database (Manual)</string>
|
||||||
|
<string name="content_desc_delete_icon">Delete icon</string>
|
||||||
|
<string name="dialog_title_select_backup_directory">Select Backup Directory</string>
|
||||||
|
<string name="settings_backup_location_select_action">Select Backup Location</string>
|
||||||
|
<string name="settings_backup_location_selected_folder">Selected folder</string>
|
||||||
|
<string name="settings_backup_location_open_error_no_app">No application found to open the folder.</string>
|
||||||
|
<string name="settings_backup_location_open_error">Could not open backup location.</string>
|
||||||
|
<string name="settings_backup_location_selected_toast">Backup location set to: %1$s</string>
|
||||||
|
<string name="settings_backup_location_selection_cancelled">Folder selection cancelled. Automatic backup not enabled.</string>
|
||||||
|
<string name="settings_last_backup_status_successful">Last backup: %1$s</string>
|
||||||
|
<string name="settings_last_backup_status_never">Last backup: Never</string>
|
||||||
|
<string name="interval_daily">Daily</string>
|
||||||
|
<string name="interval_weekly">Weekly</string>
|
||||||
|
<string name="interval_monthly">Monthly</string>
|
||||||
|
|
||||||
<!-- Data Management Dialog Titles -->
|
<!-- Data Management Dialog Titles -->
|
||||||
<string name="dialog_title_export_select_user">Export: Select User</string>
|
<string name="dialog_title_export_select_user">Export: Select User</string>
|
||||||
|
@@ -1,17 +1,19 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.11.0"
|
agp = "8.11.0"
|
||||||
kotlin = "2.2.0"
|
kotlin = "2.2.0"
|
||||||
coreKtx = "1.16.0"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.3.0"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.7.0"
|
||||||
lifecycleRuntimeKtx = "2.9.1"
|
lifecycleRuntimeKtx = "2.9.2"
|
||||||
activityCompose = "1.10.1"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2025.06.01"
|
composeBom = "2025.08.00"
|
||||||
room = "2.7.2"
|
room = "2.7.2"
|
||||||
navigation = "2.9.1"
|
navigation = "2.9.3"
|
||||||
lifecycle = "2.9.1"
|
lifecycle = "2.9.2"
|
||||||
datastore = "1.1.7"
|
datastore = "1.1.7"
|
||||||
|
worker="2.10.3"
|
||||||
|
documentfile = "1.1.0"
|
||||||
composeCharts = "2.1.3"
|
composeCharts = "2.1.3"
|
||||||
composeReorderable = "2.5.1"
|
composeReorderable = "2.5.1"
|
||||||
compose-material = "1.7.8"
|
compose-material = "1.7.8"
|
||||||
@@ -41,6 +43,8 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
|||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version = "lifecycle" }
|
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version = "lifecycle" }
|
||||||
|
androidx-worker = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "worker" }
|
||||||
|
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
|
||||||
compose-charts = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "composeCharts" }
|
compose-charts = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "composeCharts" }
|
||||||
compose-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "composeCharts" }
|
compose-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "composeCharts" }
|
||||||
compose-reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "composeReorderable" }
|
compose-reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "composeReorderable" }
|
||||||
|
Reference in New Issue
Block a user