BackupManager

closes: #4
This commit is contained in:
Sad Ellie 2023-12-02 17:53:12 +03:00
parent 7e2d4d8f3f
commit f7a89594fa
22 changed files with 1055 additions and 137 deletions

View File

@ -19,6 +19,7 @@
package com.sadellie.unitto.core.ui.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@ -39,6 +40,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
fun UnittoScreenWithLargeTopBar(
title: String,
navigationIcon: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
content: @Composable (PaddingValues) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
@ -53,7 +55,8 @@ fun UnittoScreenWithLargeTopBar(
Text(text = title)
},
navigationIcon = navigationIcon,
scrollBehavior = scrollBehavior
scrollBehavior = scrollBehavior,
actions = actions
)
},
content = content

1
data/backup/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,36 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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/>.
*/
plugins {
id("unitto.library")
id("unitto.android.library.jacoco")
id("unitto.android.hilt")
}
android {
namespace = "com.sadellie.unitto.data.backup"
}
dependencies {
implementation(libs.androidx.datastore.datastore.preferences)
implementation(libs.com.squareup.moshi.moshi.kotlin)
implementation(project(":data:database"))
implementation(project(":data:model"))
implementation(project(":data:userprefs"))
}

View File

View File

@ -0,0 +1,206 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.backup
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.sadellie.unitto.data.userprefs.PrefsKeys
import com.sadellie.unitto.data.userprefs.getThemingMode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class BackupManagerTest {
private lateinit var dataStore: DataStore<Preferences>
private lateinit var backupManager: BackupManager
@Before
fun setup() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
dataStore = PreferenceDataStoreFactory.create(
corruptionHandler = null,
produceFile = { appContext.preferencesDataStoreFile("test") }
)
backupManager = BackupManager(
mContext = appContext,
dataStore = dataStore,
unitsDao = FakeUnitsDao,
timeZoneDao = FakeTimeZoneDao
)
runBlocking {
dataStore.edit {
it[PrefsKeys.THEMING_MODE] = FakeUsrPreferenceValues.themingMode
it[PrefsKeys.ENABLE_DYNAMIC_THEME] = FakeUsrPreferenceValues.enableDynamicTheme
it[PrefsKeys.ENABLE_AMOLED_THEME] = FakeUsrPreferenceValues.enableAmoledTheme
it[PrefsKeys.CUSTOM_COLOR] = FakeUsrPreferenceValues.customColor
it[PrefsKeys.MONET_MODE] = FakeUsrPreferenceValues.monetMode
it[PrefsKeys.STARTING_SCREEN] = FakeUsrPreferenceValues.startingScreen
it[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] = FakeUsrPreferenceValues.enableToolsExperiment
it[PrefsKeys.ENABLE_VIBRATIONS] = FakeUsrPreferenceValues.enableVibrations
it[PrefsKeys.MIDDLE_ZERO] = FakeUsrPreferenceValues.middleZero
it[PrefsKeys.AC_BUTTON] = FakeUsrPreferenceValues.acButton
it[PrefsKeys.RPN_MODE] = FakeUsrPreferenceValues.rpnMode
// FORMATTER
it[PrefsKeys.DIGITS_PRECISION] = FakeUsrPreferenceValues.precision
it[PrefsKeys.SEPARATOR] = FakeUsrPreferenceValues.separator
it[PrefsKeys.OUTPUT_FORMAT] = FakeUsrPreferenceValues.outputFormat
// CALCULATOR
it[PrefsKeys.RADIAN_MODE] = FakeUsrPreferenceValues.radianMode
it[PrefsKeys.PARTIAL_HISTORY_VIEW] = FakeUsrPreferenceValues.partialHistoryView
it[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] = FakeUsrPreferenceValues.clearInputAfterEquals
// UNIT CONVERTER
it[PrefsKeys.LATEST_LEFT_SIDE] = FakeUsrPreferenceValues.latestLeftSide
it[PrefsKeys.LATEST_RIGHT_SIDE] = FakeUsrPreferenceValues.latestRightSide
it[PrefsKeys.SHOWN_UNIT_GROUPS] = FakeUsrPreferenceValues.shownUnitGroups.joinToString(",")
it[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] = FakeUsrPreferenceValues.unitConverterFavoritesOnly
it[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] = FakeUsrPreferenceValues.unitConverterFormatTime
it[PrefsKeys.UNIT_CONVERTER_SORTING] = FakeUsrPreferenceValues.unitConverterSorting.name
}
}
}
@Test
fun getUserDataTest() = runBlocking{
val actualUserData = backupManager.userDataFromApp()
val expectedUserData = UserData(
themingMode = FakeUsrPreferenceValues.themingMode,
enableDynamicTheme = FakeUsrPreferenceValues.enableDynamicTheme,
enableAmoledTheme = FakeUsrPreferenceValues.enableAmoledTheme,
customColor = FakeUsrPreferenceValues.customColor,
monetMode = FakeUsrPreferenceValues.monetMode,
startingScreen = FakeUsrPreferenceValues.startingScreen,
enableToolsExperiment = FakeUsrPreferenceValues.enableToolsExperiment,
enableVibrations = FakeUsrPreferenceValues.enableVibrations,
middleZero = FakeUsrPreferenceValues.middleZero,
acButton = FakeUsrPreferenceValues.acButton,
rpnMode = FakeUsrPreferenceValues.rpnMode,
precision = FakeUsrPreferenceValues.precision,
separator = FakeUsrPreferenceValues.separator,
outputFormat = FakeUsrPreferenceValues.outputFormat,
radianMode = FakeUsrPreferenceValues.radianMode,
partialHistoryView = FakeUsrPreferenceValues.partialHistoryView,
clearInputAfterEquals = FakeUsrPreferenceValues.clearInputAfterEquals,
latestLeftSide = FakeUsrPreferenceValues.latestLeftSide,
latestRightSide = FakeUsrPreferenceValues.latestRightSide,
shownUnitGroups = FakeUsrPreferenceValues.shownUnitGroups,
unitConverterFavoritesOnly = FakeUsrPreferenceValues.unitConverterFavoritesOnly,
unitConverterFormatTime = FakeUsrPreferenceValues.unitConverterFormatTime,
unitConverterSorting = FakeUsrPreferenceValues.unitConverterSorting,
unitsTable = units,
timeZoneTable = timeZones
)
assertEquals(expectedUserData, actualUserData)
}
@Test
fun updateDatastoreTest() = runBlocking{
backupManager.updateDatastore(
UserData(
themingMode = FakeUsrPreferenceValues.themingMode,
enableDynamicTheme = FakeUsrPreferenceValues.enableDynamicTheme,
enableAmoledTheme = FakeUsrPreferenceValues.enableAmoledTheme,
customColor = FakeUsrPreferenceValues.customColor,
monetMode = FakeUsrPreferenceValues.monetMode,
startingScreen = FakeUsrPreferenceValues.startingScreen,
enableToolsExperiment = FakeUsrPreferenceValues.enableToolsExperiment,
enableVibrations = FakeUsrPreferenceValues.enableVibrations,
middleZero = FakeUsrPreferenceValues.middleZero,
acButton = FakeUsrPreferenceValues.acButton,
rpnMode = FakeUsrPreferenceValues.rpnMode,
precision = FakeUsrPreferenceValues.precision,
separator = FakeUsrPreferenceValues.separator,
outputFormat = FakeUsrPreferenceValues.outputFormat,
radianMode = FakeUsrPreferenceValues.radianMode,
partialHistoryView = FakeUsrPreferenceValues.partialHistoryView,
clearInputAfterEquals = FakeUsrPreferenceValues.clearInputAfterEquals,
latestLeftSide = FakeUsrPreferenceValues.latestLeftSide,
latestRightSide = FakeUsrPreferenceValues.latestRightSide,
shownUnitGroups = FakeUsrPreferenceValues.shownUnitGroups,
unitConverterFavoritesOnly = FakeUsrPreferenceValues.unitConverterFavoritesOnly,
unitConverterFormatTime = FakeUsrPreferenceValues.unitConverterFormatTime,
unitConverterSorting = FakeUsrPreferenceValues.unitConverterSorting,
unitsTable = units,
timeZoneTable = timeZones
)
)
val data = dataStore.data.first()
// TODO Wrong implementation, should test all
assertEquals(FakeUsrPreferenceValues.themingMode, data.getThemingMode())
}
@Test
fun updateUnitsTableTest() = runBlocking {
backupManager.updateUnitsTable(
UserData(
themingMode = FakeUsrPreferenceValues.themingMode,
enableDynamicTheme = FakeUsrPreferenceValues.enableDynamicTheme,
enableAmoledTheme = FakeUsrPreferenceValues.enableAmoledTheme,
customColor = FakeUsrPreferenceValues.customColor,
monetMode = FakeUsrPreferenceValues.monetMode,
startingScreen = FakeUsrPreferenceValues.startingScreen,
enableToolsExperiment = FakeUsrPreferenceValues.enableToolsExperiment,
enableVibrations = FakeUsrPreferenceValues.enableVibrations,
middleZero = FakeUsrPreferenceValues.middleZero,
acButton = FakeUsrPreferenceValues.acButton,
rpnMode = FakeUsrPreferenceValues.rpnMode,
precision = FakeUsrPreferenceValues.precision,
separator = FakeUsrPreferenceValues.separator,
outputFormat = FakeUsrPreferenceValues.outputFormat,
radianMode = FakeUsrPreferenceValues.radianMode,
partialHistoryView = FakeUsrPreferenceValues.partialHistoryView,
clearInputAfterEquals = FakeUsrPreferenceValues.clearInputAfterEquals,
latestLeftSide = FakeUsrPreferenceValues.latestLeftSide,
latestRightSide = FakeUsrPreferenceValues.latestRightSide,
shownUnitGroups = FakeUsrPreferenceValues.shownUnitGroups,
unitConverterFavoritesOnly = FakeUsrPreferenceValues.unitConverterFavoritesOnly,
unitConverterFormatTime = FakeUsrPreferenceValues.unitConverterFormatTime,
unitConverterSorting = FakeUsrPreferenceValues.unitConverterSorting,
unitsTable = emptyList(),
timeZoneTable = timeZones
)
)
val data = FakeUnitsDao.getAllFlow().first()
// TODO Wrong implementation
assertEquals("$units | $data", units, data)
}
}

View File

@ -0,0 +1,54 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.backup
import com.sadellie.unitto.data.database.TimeZoneDao
import com.sadellie.unitto.data.database.TimeZoneEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
val timeZones = listOf(
TimeZoneEntity(
id = "id1",
position = 9,
label = "label"
),
TimeZoneEntity(
id = "id2",
position = 9,
label = "label"
)
)
object FakeTimeZoneDao: TimeZoneDao {
override fun getFavorites(): Flow<List<TimeZoneEntity>> {
return flow {
emit(timeZones)
}
}
override fun getMaxPosition(): Int = 0
override suspend fun insert(vararg timeZoneEntity: TimeZoneEntity) {}
override suspend fun removeFromFavorites(id: String) {}
override suspend fun updateLabel(id: String, label: String) {}
override suspend fun updateDragged(id: String, oldPosition: Int, newPosition: Int) {}
override suspend fun moveDown(currentPosition: Int, targetPosition: Int) {}
override suspend fun moveUp(currentPosition: Int, targetPosition: Int) {}
override suspend fun clear() {}
}

View File

@ -0,0 +1,51 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.backup
import com.sadellie.unitto.data.database.UnitsDao
import com.sadellie.unitto.data.database.UnitsEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
val units = listOf(
UnitsEntity(
unitId = "UnitId1",
isFavorite = false,
pairedUnitId = "Pair",
frequency = 9
),
UnitsEntity(
unitId = "UnitId2",
isFavorite = false,
pairedUnitId = "Pair",
frequency = 9
)
)
object FakeUnitsDao : UnitsDao {
override fun getAllFlow(): Flow<List<UnitsEntity>> {
return flow {
emit(units)
}
}
override suspend fun insertUnit(unit: UnitsEntity) {}
override suspend fun getById(unitId: String): UnitsEntity? = null
override suspend fun clear() {}
}

View File

@ -0,0 +1,48 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.backup
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitsListSorting
object FakeUsrPreferenceValues {
const val themingMode = "ThemingMode"
const val enableDynamicTheme = false
const val enableAmoledTheme = false
const val customColor = 777L
const val monetMode = "MonetMode"
const val startingScreen = "StartingScreen"
const val enableToolsExperiment = false
const val enableVibrations = false
const val middleZero = false
const val acButton = false
const val rpnMode = false
const val precision = 69
const val separator = 1
const val outputFormat = 1
const val radianMode = false
const val partialHistoryView = false
const val clearInputAfterEquals = false
const val latestLeftSide = "LeftSideUnit"
const val latestRightSide = "RightSideUnit"
val shownUnitGroups = ALL_UNIT_GROUPS
const val unitConverterFavoritesOnly = false
const val unitConverterFormatTime = false
val unitConverterSorting = UnitsListSorting.USAGE
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Unitto is a unit converter for Android
~ Copyright (c) 2023 Elshan Agaev
~
~ 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.sadellie.unitto.BackupManager"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_path"/>
</provider>
</application>
</manifest>

View File

@ -0,0 +1,191 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.backup
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.sadellie.unitto.data.database.TimeZoneDao
import com.sadellie.unitto.data.database.UnitsDao
import com.sadellie.unitto.data.userprefs.PrefsKeys
import com.sadellie.unitto.data.userprefs.getAcButton
import com.sadellie.unitto.data.userprefs.getClearInputAfterEquals
import com.sadellie.unitto.data.userprefs.getCustomColor
import com.sadellie.unitto.data.userprefs.getDigitsPrecision
import com.sadellie.unitto.data.userprefs.getEnableAmoledTheme
import com.sadellie.unitto.data.userprefs.getEnableDynamicTheme
import com.sadellie.unitto.data.userprefs.getEnableToolsExperiment
import com.sadellie.unitto.data.userprefs.getEnableVibrations
import com.sadellie.unitto.data.userprefs.getLatestLeftSide
import com.sadellie.unitto.data.userprefs.getLatestRightSide
import com.sadellie.unitto.data.userprefs.getMiddleZero
import com.sadellie.unitto.data.userprefs.getMonetMode
import com.sadellie.unitto.data.userprefs.getOutputFormat
import com.sadellie.unitto.data.userprefs.getPartialHistoryView
import com.sadellie.unitto.data.userprefs.getRadianMode
import com.sadellie.unitto.data.userprefs.getRpnMode
import com.sadellie.unitto.data.userprefs.getSeparator
import com.sadellie.unitto.data.userprefs.getShownUnitGroups
import com.sadellie.unitto.data.userprefs.getStartingScreen
import com.sadellie.unitto.data.userprefs.getThemingMode
import com.sadellie.unitto.data.userprefs.getUnitConverterFavoritesOnly
import com.sadellie.unitto.data.userprefs.getUnitConverterFormatTime
import com.sadellie.unitto.data.userprefs.getUnitConverterSorting
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import javax.inject.Inject
class BackupManager @Inject constructor(
@ApplicationContext private val mContext: Context,
private val dataStore: DataStore<Preferences>,
private val unitsDao: UnitsDao,
private val timeZoneDao: TimeZoneDao,
// Not planned at the moment
// private val calculatorHistoryDao: CalculatorHistoryDao,
) {
private val moshi: Moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
private val jsonAdapter: JsonAdapter<UserData> = moshi.adapter(UserData::class.java)
private val auth = "com.sadellie.unitto.BackupManager"
suspend fun backup(): Uri = withContext(Dispatchers.IO) {
val userData = userDataFromApp()
// save to disk
val tempFile = File.createTempFile("backup", ".unitto", mContext.cacheDir)
tempFile.writeText(jsonAdapter.toJson(userData))
return@withContext FileProvider.getUriForFile(mContext, auth, tempFile)
}
suspend fun restore(uri: Uri) = withContext(Dispatchers.IO) {
val jsonContent = StringBuilder()
mContext.contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
var line: String? = reader.readLine()
while (line != null) {
jsonContent.append(line)
line = reader.readLine()
}
}
}
// Return error
val userData: UserData = jsonAdapter.fromJson(jsonContent.toString()) ?: return@withContext IllegalArgumentException("Can't parse: $jsonContent")
// Apply tables
updateUnitsTable(userData)
updateTimeZonesTable(userData)
// Apply datastore prefs
// Datastore settings are restored at the end, because it will trigger recomposition of the
// entire app composable and all jobs in ViewModels will be canceled
updateDatastore(userData)
}
internal suspend fun userDataFromApp(): UserData {
val data = dataStore.data.first()
val unitsTableData = unitsDao.getAllFlow().first()
val timeZoneTableData = timeZoneDao.getFavorites().first()
return UserData(
themingMode = data.getThemingMode(),
enableDynamicTheme = data.getEnableDynamicTheme(),
enableAmoledTheme = data.getEnableAmoledTheme(),
customColor = data.getCustomColor(),
monetMode = data.getMonetMode(),
startingScreen = data.getStartingScreen(),
enableToolsExperiment = data.getEnableToolsExperiment(),
enableVibrations = data.getEnableVibrations(),
middleZero = data.getMiddleZero(),
acButton = data.getAcButton(),
rpnMode = data.getRpnMode(),
precision = data.getDigitsPrecision(),
separator = data.getSeparator(),
outputFormat = data.getOutputFormat(),
radianMode = data.getRadianMode(),
partialHistoryView = data.getPartialHistoryView(),
clearInputAfterEquals = data.getClearInputAfterEquals(),
latestLeftSide = data.getLatestLeftSide(),
latestRightSide = data.getLatestRightSide(),
shownUnitGroups = data.getShownUnitGroups(),
unitConverterFavoritesOnly = data.getUnitConverterFavoritesOnly(),
unitConverterFormatTime = data.getUnitConverterFormatTime(),
unitConverterSorting = data.getUnitConverterSorting(),
unitsTable = unitsTableData,
timeZoneTable = timeZoneTableData,
)
}
internal suspend fun updateDatastore(userData: UserData) {
dataStore.edit { it.clear() }
dataStore.edit {
it[PrefsKeys.THEMING_MODE] = userData.themingMode
it[PrefsKeys.ENABLE_DYNAMIC_THEME] = userData.enableDynamicTheme
it[PrefsKeys.ENABLE_AMOLED_THEME] = userData.enableAmoledTheme
it[PrefsKeys.CUSTOM_COLOR] = userData.customColor
it[PrefsKeys.MONET_MODE] = userData.monetMode
it[PrefsKeys.STARTING_SCREEN] = userData.startingScreen
it[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] = userData.enableToolsExperiment
it[PrefsKeys.ENABLE_VIBRATIONS] = userData.enableVibrations
it[PrefsKeys.MIDDLE_ZERO] = userData.middleZero
it[PrefsKeys.AC_BUTTON] = userData.acButton
it[PrefsKeys.RPN_MODE] = userData.rpnMode
// FORMATTER
it[PrefsKeys.DIGITS_PRECISION] = userData.precision
it[PrefsKeys.SEPARATOR] = userData.separator
it[PrefsKeys.OUTPUT_FORMAT] = userData.outputFormat
// CALCULATOR
it[PrefsKeys.RADIAN_MODE] = userData.radianMode
it[PrefsKeys.PARTIAL_HISTORY_VIEW] = userData.partialHistoryView
it[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] = userData.clearInputAfterEquals
// UNIT CONVERTER
it[PrefsKeys.LATEST_LEFT_SIDE] = userData.latestLeftSide
it[PrefsKeys.LATEST_RIGHT_SIDE] = userData.latestRightSide
it[PrefsKeys.SHOWN_UNIT_GROUPS] = userData.shownUnitGroups.joinToString(",")
it[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] = userData.unitConverterFavoritesOnly
it[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] = userData.unitConverterFormatTime
it[PrefsKeys.UNIT_CONVERTER_SORTING] = userData.unitConverterSorting.name
}
}
internal suspend fun updateUnitsTable(userData: UserData) {
unitsDao.clear()
userData.unitsTable.forEach { unitsDao.insertUnit(it) }
}
internal suspend fun updateTimeZonesTable(userData: UserData) {
timeZoneDao.clear()
userData.timeZoneTable.forEach { timeZoneDao.insert(it) }
}
}

View File

@ -0,0 +1,57 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.backup
import com.sadellie.unitto.data.database.TimeZoneEntity
import com.sadellie.unitto.data.database.UnitsEntity
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
// Don't move to model module. This uses entity classes from database module
data class UserData(
val themingMode: String,
val enableDynamicTheme: Boolean,
val enableAmoledTheme: Boolean,
val customColor: Long,
val monetMode: String,
val startingScreen: String,
val enableToolsExperiment: Boolean,
val enableVibrations: Boolean,
val middleZero: Boolean,
val acButton: Boolean,
val rpnMode: Boolean,
val precision: Int,
val separator: Int,
val outputFormat: Int,
val radianMode: Boolean,
val partialHistoryView: Boolean,
val clearInputAfterEquals: Boolean,
val latestLeftSide: String,
val latestRightSide: String,
val shownUnitGroups: List<UnitGroup>,
val unitConverterFavoritesOnly: Boolean,
val unitConverterFormatTime: Boolean,
val unitConverterSorting: UnitsListSorting,
val unitsTable: List<UnitsEntity>,
val timeZoneTable: List<TimeZoneEntity>,
)

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Unitto is a unit converter for Android
~ Copyright (c) 2023 Elshan Agaev
~
~ 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/>.
-->
<paths>
<cache-path name="cache" path="."/>
</paths>

View File

@ -74,4 +74,7 @@ interface TimeZoneDao {
}
updateDragged(id, 0, targetPosition)
}
@Query("DELETE FROM time_zones")
suspend fun clear()
}

View File

@ -35,4 +35,7 @@ interface UnitsDao {
@Query("SELECT * FROM units WHERE unitId == :unitId LIMIT 1")
suspend fun getById(unitId: String): UnitsEntity?
@Query("DELETE FROM units")
suspend fun clear()
}

View File

@ -0,0 +1,135 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.data.userprefs
import androidx.datastore.preferences.core.Preferences
import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.units.MyUnitIDS
fun Preferences.getEnableDynamicTheme(): Boolean {
return this[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: true
}
fun Preferences.getThemingMode(): String {
return this[PrefsKeys.THEMING_MODE] ?: ""
}
fun Preferences.getEnableAmoledTheme(): Boolean {
return this[PrefsKeys.ENABLE_AMOLED_THEME] ?: false
}
fun Preferences.getCustomColor(): Long {
return this[PrefsKeys.CUSTOM_COLOR] ?: 16L // From Color.Unspecified
}
fun Preferences.getMonetMode(): String {
return this[PrefsKeys.MONET_MODE] ?: ""
}
fun Preferences.getStartingScreen(): String {
return this[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Calculator.graph
}
fun Preferences.getEnableToolsExperiment(): Boolean {
return this[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
}
fun Preferences.getEnableVibrations(): Boolean {
return this[PrefsKeys.ENABLE_VIBRATIONS] ?: true
}
fun Preferences.getRadianMode(): Boolean {
return this[PrefsKeys.RADIAN_MODE] ?: true
}
fun Preferences.getSeparator(): Int {
return this[PrefsKeys.SEPARATOR] ?: Separator.SPACE
}
fun Preferences.getMiddleZero(): Boolean {
return this[PrefsKeys.MIDDLE_ZERO] ?: true
}
fun Preferences.getPartialHistoryView(): Boolean {
return this[PrefsKeys.PARTIAL_HISTORY_VIEW] ?: true
}
fun Preferences.getDigitsPrecision(): Int {
return this[PrefsKeys.DIGITS_PRECISION] ?: 3
}
fun Preferences.getOutputFormat(): Int {
return this[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
}
fun Preferences.getUnitConverterFormatTime(): Boolean {
return this[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
}
fun Preferences.getUnitConverterSorting(): UnitsListSorting {
return this[PrefsKeys.UNIT_CONVERTER_SORTING]
?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
}
fun Preferences.getShownUnitGroups(): List<UnitGroup> {
return this[PrefsKeys.SHOWN_UNIT_GROUPS]
?.letTryOrNull { list ->
list
.ifEmpty { return@letTryOrNull listOf() }
.split(",")
.map { UnitGroup.valueOf(it) }
}
?: ALL_UNIT_GROUPS
}
fun Preferences.getUnitConverterFavoritesOnly(): Boolean {
return this[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY]
?: false
}
fun Preferences.getLatestLeftSide(): String {
return this[PrefsKeys.LATEST_LEFT_SIDE] ?: MyUnitIDS.kilometer
}
fun Preferences.getLatestRightSide(): String {
return this[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
}
fun Preferences.getAcButton(): Boolean {
return this[PrefsKeys.AC_BUTTON] ?: true
}
fun Preferences.getClearInputAfterEquals(): Boolean {
return this[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] ?: true
}
fun Preferences.getRpnMode(): Boolean {
return this[PrefsKeys.RPN_MODE] ?: false
}
private inline fun <T, R> T.letTryOrNull(block: (T) -> R): R? = try {
this?.let(block)
} catch (e: Exception) {
null
}

View File

@ -23,7 +23,7 @@ import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
internal object PrefsKeys {
object PrefsKeys {
// COMMON
val THEMING_MODE = stringPreferencesKey("THEMING_MODE_PREF_KEY")
val ENABLE_DYNAMIC_THEME = booleanPreferencesKey("ENABLE_DYNAMIC_THEME_PREF_KEY")

View File

@ -22,10 +22,6 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
@ -40,7 +36,6 @@ import com.sadellie.unitto.data.model.userprefs.FormattingPreferences
import com.sadellie.unitto.data.model.userprefs.GeneralPreferences
import com.sadellie.unitto.data.model.userprefs.StartingScreenPreferences
import com.sadellie.unitto.data.model.userprefs.UnitGroupsPreferences
import com.sadellie.unitto.data.units.MyUnitIDS
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
@ -287,107 +282,3 @@ class UserPreferencesRepositoryImpl @Inject constructor(
}
}
}
private fun Preferences.getEnableDynamicTheme(): Boolean {
return this[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: true
}
private fun Preferences.getThemingMode(): String {
return this[PrefsKeys.THEMING_MODE] ?: ""
}
private fun Preferences.getEnableAmoledTheme(): Boolean {
return this[PrefsKeys.ENABLE_AMOLED_THEME] ?: false
}
private fun Preferences.getCustomColor(): Long {
return this[PrefsKeys.CUSTOM_COLOR] ?: 16L // From Color.Unspecified
}
private fun Preferences.getMonetMode(): String {
return this[PrefsKeys.MONET_MODE] ?: ""
}
private fun Preferences.getStartingScreen(): String {
return this[PrefsKeys.STARTING_SCREEN]
?: TopLevelDestinations.Calculator.graph
}
private fun Preferences.getEnableToolsExperiment(): Boolean {
return this[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
}
private fun Preferences.getEnableVibrations(): Boolean {
return this[PrefsKeys.ENABLE_VIBRATIONS] ?: true
}
private fun Preferences.getRadianMode(): Boolean {
return this[PrefsKeys.RADIAN_MODE] ?: true
}
private fun Preferences.getSeparator(): Int {
return this[PrefsKeys.SEPARATOR] ?: Separator.SPACE
}
private fun Preferences.getMiddleZero(): Boolean {
return this[PrefsKeys.MIDDLE_ZERO] ?: true
}
private fun Preferences.getPartialHistoryView(): Boolean {
return this[PrefsKeys.PARTIAL_HISTORY_VIEW] ?: true
}
private fun Preferences.getDigitsPrecision(): Int {
return this[PrefsKeys.DIGITS_PRECISION] ?: 3
}
private fun Preferences.getOutputFormat(): Int {
return this[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
}
private fun Preferences.getUnitConverterFormatTime(): Boolean {
return this[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
}
private fun Preferences.getUnitConverterSorting(): UnitsListSorting {
return this[PrefsKeys.UNIT_CONVERTER_SORTING]
?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
}
private fun Preferences.getShownUnitGroups(): List<UnitGroup> {
return this[PrefsKeys.SHOWN_UNIT_GROUPS]?.letTryOrNull { list ->
list.ifEmpty { return@letTryOrNull listOf() }.split(",")
.map { UnitGroup.valueOf(it) }
} ?: ALL_UNIT_GROUPS
}
private fun Preferences.getUnitConverterFavoritesOnly(): Boolean {
return this[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY]
?: false
}
private fun Preferences.getLatestLeftSide(): String {
return this[PrefsKeys.LATEST_LEFT_SIDE] ?: MyUnitIDS.kilometer
}
private fun Preferences.getLatestRightSide(): String {
return this[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
}
private fun Preferences.getAcButton(): Boolean {
return this[PrefsKeys.AC_BUTTON] ?: true
}
private fun Preferences.getClearInputAfterEquals(): Boolean {
return this[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] ?: true
}
private fun Preferences.getRpnMode(): Boolean {
return this[PrefsKeys.RPN_MODE] ?: false
}
private inline fun <T, R> T.letTryOrNull(block: (T) -> R): R? = try {
this?.let(block)
} catch (e: Exception) {
null
}

View File

@ -33,6 +33,7 @@ dependencies {
implementation(libs.org.burnoutcrew.composereorderable.reorderable)
implementation(libs.androidx.appcompat.appcompat)
implementation(project(":data:backup"))
implementation(project(":data:common"))
implementation(project(":data:database"))
implementation(project(":data:model"))

View File

@ -18,12 +18,21 @@
package com.sadellie.unitto.feature.settings
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -34,16 +43,27 @@ import androidx.compose.material.icons.filled.Cached
import androidx.compose.material.icons.filled.Calculate
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.RateReview
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.filled.Vibration
import androidx.compose.material.icons.filled._123
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -59,8 +79,6 @@ import com.sadellie.unitto.core.ui.common.UnittoListItem
import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
import com.sadellie.unitto.core.ui.openLink
import com.sadellie.unitto.core.ui.showToast
import com.sadellie.unitto.data.model.userprefs.GeneralPreferences
import com.sadellie.unitto.data.userprefs.GeneralPreferencesImpl
import com.sadellie.unitto.feature.settings.navigation.aboutRoute
import com.sadellie.unitto.feature.settings.navigation.calculatorSettingsRoute
import com.sadellie.unitto.feature.settings.navigation.converterSettingsRoute
@ -74,38 +92,99 @@ internal fun SettingsRoute(
navigateUp: () -> Unit,
navControllerAction: (String) -> Unit,
) {
val userPrefs = viewModel.userPrefs.collectAsStateWithLifecycle().value
val cachePercentage = viewModel.cachePercentage.collectAsStateWithLifecycle()
val mContext = LocalContext.current
when (userPrefs) {
null -> UnittoEmptyScreen()
else -> {
SettingsScreen(
userPrefs = userPrefs,
val errorLabel = stringResource(R.string.error_label)
val uiState: SettingsUIState = viewModel.uiState.collectAsStateWithLifecycle().value
val backupFileUri: Uri? = viewModel.backupFileUri.collectAsStateWithLifecycle(initialValue = null).value
val showErrorToast: Boolean = viewModel.showErrorToast.collectAsStateWithLifecycle(initialValue = false).value
// Share backup file when it's emitted
LaunchedEffect(backupFileUri) {
if (backupFileUri == null) return@LaunchedEffect
mContext.share(backupFileUri)
}
LaunchedEffect(showErrorToast) {
if (showErrorToast) Toast.makeText(mContext, errorLabel, Toast.LENGTH_SHORT).show()
}
Crossfade(targetState = uiState) { state ->
when (state) {
SettingsUIState.Loading -> UnittoEmptyScreen()
SettingsUIState.BackupInProgress -> BackingUpScreen()
is SettingsUIState.Ready -> SettingsScreen(
uiState = state,
navigateUp = navigateUp,
navControllerAction = navControllerAction,
updateVibrations = viewModel::updateVibrations,
cachePercentage = cachePercentage.value,
clearCache = viewModel::clearCache,
backup = viewModel::backup,
restore = viewModel::restore
)
}
}
}
@Composable
private fun BackingUpScreen() {
Scaffold { padding ->
Box(
modifier = Modifier
.padding(padding)
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
BackHandler {}
}
@Composable
private fun SettingsScreen(
userPrefs: GeneralPreferences,
uiState: SettingsUIState.Ready,
navigateUp: () -> Unit,
navControllerAction: (String) -> Unit,
updateVibrations: (Boolean) -> Unit,
cachePercentage: Float,
clearCache: () -> Unit,
backup: () -> Unit,
restore: (Uri) -> Unit = {},
) {
val mContext = LocalContext.current
var showMenu by remember { mutableStateOf(false) }
// Pass picked file uri to BackupManager
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { pickedUri ->
if (pickedUri != null) restore(pickedUri)
}
UnittoScreenWithLargeTopBar(
title = stringResource(R.string.settings_title),
navigationIcon = { NavigateUpButton(navigateUp) }
navigationIcon = { NavigateUpButton(navigateUp) },
actions = {
IconButton(
onClick = { showMenu = !showMenu },
content = { Icon(Icons.Default.MoreVert, null) }
)
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
onClick = backup,
text = { Text("Backup") }
)
DropdownMenuItem(
onClick = { launcher.launch(arrayOf(backupMimeType)) },
text = { Text("Restore") }
)
}
}
) { padding ->
Column(
modifier = Modifier
@ -155,12 +234,12 @@ private fun SettingsScreen(
headlineText = stringResource(R.string.settings_vibrations),
supportingText = stringResource(R.string.settings_vibrations_support),
modifier = Modifier.clickable { navControllerAction(converterSettingsRoute) },
switchState = userPrefs.enableVibrations,
switchState = uiState.enableVibrations,
onSwitchChange = updateVibrations
)
AnimatedVisibility(
visible = cachePercentage > 0,
visible = uiState.cacheSize > 0,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(),
) {
@ -189,19 +268,38 @@ private fun SettingsScreen(
}
}
private fun Context.share(uri: Uri) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = backupMimeType // This is a fucking war crime, it should be text/json
}
startActivity(shareIntent)
}
private const val backupMimeType = "application/octet-stream"
@Preview
@Composable
private fun PreviewSettingsScreen() {
var cacheSize by remember { mutableFloatStateOf(0.9f) }
SettingsScreen(
userPrefs = GeneralPreferencesImpl(
enableVibrations = true
uiState = SettingsUIState.Ready(
enableVibrations = false,
cacheSize = 2,
),
navigateUp = {},
navControllerAction = {},
updateVibrations = {},
cachePercentage = cacheSize,
clearCache = { cacheSize = 0f }
clearCache = { cacheSize = 0f },
backup = {}
)
}
@Preview
@Composable
private fun PreviewBackingUpScreen() {
BackingUpScreen()
}

View File

@ -0,0 +1,30 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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.sadellie.unitto.feature.settings
internal sealed class SettingsUIState {
data object Loading : SettingsUIState()
data object BackupInProgress : SettingsUIState()
data class Ready(
val enableVibrations: Boolean,
val cacheSize: Int,
) : SettingsUIState()
}

View File

@ -18,14 +18,22 @@
package com.sadellie.unitto.feature.settings
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.backup.BackupManager
import com.sadellie.unitto.data.common.stateIn
import com.sadellie.unitto.data.database.CurrencyRatesDao
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -33,15 +41,61 @@ import javax.inject.Inject
internal class SettingsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
private val currencyRatesDao: CurrencyRatesDao,
private val backupManager: BackupManager
) : ViewModel() {
val userPrefs = userPrefsRepository.generalPrefs
.stateIn(viewModelScope, null)
private val _backupFileUri = MutableSharedFlow<Uri?>()
val backupFileUri = _backupFileUri.asSharedFlow()
val cachePercentage = currencyRatesDao.size()
.map {
(it / 100_000f).coerceIn(0f, 1f)
private val _showErrorToast = MutableSharedFlow<Boolean>()
val showErrorToast = _showErrorToast.asSharedFlow()
private val _operation = MutableStateFlow(false)
private var backupJob: Job? = null
val uiState = combine(
userPrefsRepository.generalPrefs,
currencyRatesDao.size(),
_operation,
) { prefs, cacheSize, operation ->
if (operation) return@combine SettingsUIState.BackupInProgress
SettingsUIState.Ready(
enableVibrations = prefs.enableVibrations,
cacheSize = cacheSize,
)
}
.stateIn(viewModelScope, SettingsUIState.Loading)
fun backup() {
backupJob?.cancel()
backupJob = viewModelScope.launch(Dispatchers.IO) {
_operation.update { true }
try {
val backupFileUri = backupManager.backup()
_backupFileUri.emit(backupFileUri) // Emit to trigger file share intent
_showErrorToast.emit(false)
} catch (e: Exception) {
_showErrorToast.emit(true)
Log.e(TAG, "$e")
}
_operation.update { false }
}
}
fun restore(uri: Uri) {
backupJob?.cancel()
backupJob = viewModelScope.launch(Dispatchers.IO) {
_operation.update { true }
try {
backupManager.restore(uri)
_showErrorToast.emit(false)
} catch (e: Exception) {
_showErrorToast.emit(true)
Log.e(TAG, "$e")
}
_operation.update { false }
}
}
.stateIn(viewModelScope, 0f)
/**
* @see UserPreferencesRepository.updateVibrations
@ -54,3 +108,5 @@ internal class SettingsViewModel @Inject constructor(
currencyRatesDao.clear()
}
}
private const val TAG = "SettingsViewModel"

View File

@ -34,3 +34,4 @@ include(":data:model")
include(":data:common")
include(":data:evaluatto")
include(":data:timezone")
include(":data:backup")