diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithLargeTopBar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithLargeTopBar.kt
index 4cddcc93..f5fc3ef0 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithLargeTopBar.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithLargeTopBar.kt
@@ -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
diff --git a/data/backup/.gitignore b/data/backup/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/data/backup/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/backup/build.gradle.kts b/data/backup/build.gradle.kts
new file mode 100644
index 00000000..a3d51364
--- /dev/null
+++ b/data/backup/build.gradle.kts
@@ -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 .
+ */
+
+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"))
+}
diff --git a/data/backup/consumer-rules.pro b/data/backup/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt
new file mode 100644
index 00000000..4173a952
--- /dev/null
+++ b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt
@@ -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 .
+ */
+
+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
+ 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)
+ }
+}
diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt
new file mode 100644
index 00000000..25cb4ef3
--- /dev/null
+++ b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt
@@ -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 .
+ */
+
+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> {
+ 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() {}
+}
\ No newline at end of file
diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt
new file mode 100644
index 00000000..a1a6dfc0
--- /dev/null
+++ b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt
@@ -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 .
+ */
+
+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> {
+ return flow {
+ emit(units)
+ }
+ }
+
+ override suspend fun insertUnit(unit: UnitsEntity) {}
+ override suspend fun getById(unitId: String): UnitsEntity? = null
+ override suspend fun clear() {}
+}
\ No newline at end of file
diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt
new file mode 100644
index 00000000..0abb45bd
--- /dev/null
+++ b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt
@@ -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 .
+ */
+
+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
+}
diff --git a/data/backup/src/main/AndroidManifest.xml b/data/backup/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..6a27ed23
--- /dev/null
+++ b/data/backup/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt
new file mode 100644
index 00000000..7dc76001
--- /dev/null
+++ b/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt
@@ -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 .
+ */
+
+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,
+ 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 = 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) }
+ }
+}
diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt
new file mode 100644
index 00000000..e76582f1
--- /dev/null
+++ b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt
@@ -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 .
+ */
+
+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,
+ val unitConverterFavoritesOnly: Boolean,
+ val unitConverterFormatTime: Boolean,
+ val unitConverterSorting: UnitsListSorting,
+
+ val unitsTable: List,
+ val timeZoneTable: List,
+)
diff --git a/data/backup/src/main/res/xml/file_provider_path.xml b/data/backup/src/main/res/xml/file_provider_path.xml
new file mode 100644
index 00000000..ac221c89
--- /dev/null
+++ b/data/backup/src/main/res/xml/file_provider_path.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt
index 1984def3..ff971b51 100644
--- a/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt
+++ b/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt
@@ -74,4 +74,7 @@ interface TimeZoneDao {
}
updateDragged(id, 0, targetPosition)
}
+
+ @Query("DELETE FROM time_zones")
+ suspend fun clear()
}
diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/UnitsDao.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/UnitsDao.kt
index 5e03da1c..659a5110 100644
--- a/data/database/src/main/java/com/sadellie/unitto/data/database/UnitsDao.kt
+++ b/data/database/src/main/java/com/sadellie/unitto/data/database/UnitsDao.kt
@@ -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()
}
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceExt.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceExt.kt
new file mode 100644
index 00000000..33c06724
--- /dev/null
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceExt.kt
@@ -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 .
+ */
+
+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 {
+ 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.letTryOrNull(block: (T) -> R): R? = try {
+ this?.let(block)
+} catch (e: Exception) {
+ null
+}
\ No newline at end of file
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt
index 5ea204db..2b01559a 100644
--- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt
@@ -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")
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
index 0d722d04..7296a273 100644
--- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
@@ -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 {
- 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.letTryOrNull(block: (T) -> R): R? = try {
- this?.let(block)
-} catch (e: Exception) {
- null
-}
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
index 7624c35e..85d45651 100644
--- a/feature/settings/build.gradle.kts
+++ b/feature/settings/build.gradle.kts
@@ -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"))
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt
index 9c3ef599..6ae82b9c 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt
@@ -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()
+}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt
new file mode 100644
index 00000000..fa353bd9
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt
@@ -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 .
+ */
+
+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()
+}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt
index 3ec2cf04..b82234f4 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt
@@ -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()
+ val backupFileUri = _backupFileUri.asSharedFlow()
- val cachePercentage = currencyRatesDao.size()
- .map {
- (it / 100_000f).coerceIn(0f, 1f)
+ private val _showErrorToast = MutableSharedFlow()
+ 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 }
}
- .stateIn(viewModelScope, 0f)
+ }
+
+ 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 }
+ }
+ }
/**
* @see UserPreferencesRepository.updateVibrations
@@ -54,3 +108,5 @@ internal class SettingsViewModel @Inject constructor(
currencyRatesDao.clear()
}
}
+
+private const val TAG = "SettingsViewModel"
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d0e2a867..be9e2822 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,3 +34,4 @@ include(":data:model")
include(":data:common")
include(":data:evaluatto")
include(":data:timezone")
+include(":data:backup")