mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-19 23:12:12 +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.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.worker)
|
||||
implementation(libs.androidx.documentfile)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
@@ -19,6 +19,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".OpenScaleApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -54,6 +55,16 @@
|
||||
android:exported="true"
|
||||
android:permission="${applicationId}.READ_WRITE_DATA">
|
||||
</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>
|
||||
|
||||
</manifest>
|
@@ -50,44 +50,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point of the application.
|
||||
@@ -99,21 +61,13 @@ class MainActivity : ComponentActivity() {
|
||||
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?) {
|
||||
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 ---
|
||||
lifecycleScope.launch {
|
||||
userSettingsRepository.appLanguageCode.collectLatest { languageCode ->
|
||||
@@ -139,30 +93,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun initializeAndSetContent() {
|
||||
val db = AppDatabase.getInstance(applicationContext)
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.d(TAG, "Initializing and setting content.")
|
||||
enableEdgeToEdge()
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.text.InputType
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@@ -349,4 +350,18 @@ enum class SmoothingAlgorithm(@StringRes val displayNameResId: Int) {
|
||||
fun getDisplayName(context: android.content.Context): String {
|
||||
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 ---
|
||||
|
||||
suspend fun insertAllMeasurementTypes(types: List<MeasurementType>) {
|
||||
measurementTypeDao.insertAll(types)
|
||||
}
|
||||
|
||||
fun getAllMeasurementTypes(): Flow<List<MeasurementType>> = measurementTypeDao.getAll()
|
||||
|
||||
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.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.health.openscale.core.data.BackupInterval
|
||||
import com.health.openscale.core.data.SmoothingAlgorithm
|
||||
import com.health.openscale.core.utils.LogManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -64,6 +65,13 @@ object UserPreferenceKeys {
|
||||
val CHART_SMOOTHING_ALPHA = floatPreferencesKey("chart_smoothing_alpha")
|
||||
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)
|
||||
const val OVERVIEW_SCREEN_CONTEXT = "overview_screen"
|
||||
const val GRAPH_SCREEN_CONTEXT = "graph_screen"
|
||||
@@ -109,6 +117,22 @@ interface UserSettingsRepository {
|
||||
val chartSmoothingWindowSize: Flow<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
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
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")
|
||||
override fun <T> observeSetting(keyName: String, defaultValue: T): Flow<T> {
|
||||
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
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.application
|
||||
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.csvWriter
|
||||
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.Measurement
|
||||
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.Converters
|
||||
import com.health.openscale.core.utils.LogManager
|
||||
import com.health.openscale.core.worker.BackupWorker
|
||||
import com.health.openscale.ui.screen.SharedViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
@@ -66,6 +78,7 @@ import java.time.temporal.ChronoField
|
||||
import java.time.temporal.TemporalQueries.localDate
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
@@ -129,6 +142,22 @@ class SettingsViewModel(
|
||||
private val _isLoadingEntireDatabaseDeletion = MutableStateFlow(false)
|
||||
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 {
|
||||
private const val TAG = "SettingsViewModel"
|
||||
const val ACTION_ID_EXPORT_USER_DATA = "export_user_data"
|
||||
@@ -209,6 +238,76 @@ class SettingsViewModel(
|
||||
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) {
|
||||
viewModelScope.launch {
|
||||
_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 {
|
||||
_isLoadingRestore.value = true
|
||||
LogManager.i(TAG, "Performing database restore from URI: $restoreUri")
|
||||
@@ -988,7 +1087,7 @@ class SettingsViewModel(
|
||||
LogManager.d(TAG, "Delete entire database confirmation cancelled.")
|
||||
}
|
||||
|
||||
fun confirmDeleteEntireDatabase(applicationContext: android.content.Context) {
|
||||
fun confirmDeleteEntireDatabase(applicationContext: Context) {
|
||||
viewModelScope.launch {
|
||||
_isLoadingEntireDatabaseDeletion.value = true
|
||||
_showDeleteEntireDatabaseConfirmationDialog.value = false
|
||||
|
@@ -331,6 +331,44 @@
|
||||
<string name="settings_delete_entire_database">Gesamte Datenbank löschen</string>
|
||||
<string name="settings_danger_zone">Gefahrenzone</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 -->
|
||||
<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_danger_zone">Danger Zone</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 -->
|
||||
<string name="dialog_title_export_select_user">Export: Select User</string>
|
||||
|
@@ -1,17 +1,19 @@
|
||||
[versions]
|
||||
agp = "8.11.0"
|
||||
kotlin = "2.2.0"
|
||||
coreKtx = "1.16.0"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.9.1"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
lifecycleRuntimeKtx = "2.9.2"
|
||||
activityCompose = "1.10.1"
|
||||
composeBom = "2025.06.01"
|
||||
composeBom = "2025.08.00"
|
||||
room = "2.7.2"
|
||||
navigation = "2.9.1"
|
||||
lifecycle = "2.9.1"
|
||||
navigation = "2.9.3"
|
||||
lifecycle = "2.9.2"
|
||||
datastore = "1.1.7"
|
||||
worker="2.10.3"
|
||||
documentfile = "1.1.0"
|
||||
composeCharts = "2.1.3"
|
||||
composeReorderable = "2.5.1"
|
||||
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-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-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-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "composeCharts" }
|
||||
compose-reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "composeReorderable" }
|
||||
|
Reference in New Issue
Block a user