Rewrite BackupManager

This commit is contained in:
Sad Ellie 2024-01-19 00:20:53 +03:00
parent d9454d8d01
commit 8d46084b51
19 changed files with 224 additions and 801 deletions

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev * Copyright (c) 2023-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -17,19 +17,25 @@
*/ */
plugins { plugins {
id("com.google.devtools.ksp")
id("unitto.library") id("unitto.library")
id("unitto.android.library.jacoco")
id("unitto.android.hilt") id("unitto.android.hilt")
id("unitto.room")
id("unitto.android.library.jacoco")
} }
android.namespace = "com.sadellie.unitto.data.backup" android.namespace = "com.sadellie.unitto.data.backup"
android {
room {
val schemaLocation = "$projectDir/schemas"
schemaDirectory(schemaLocation)
println("Exported Database schema to $schemaLocation")
}
}
dependencies { dependencies {
implementation(libs.androidx.datastore.datastore.preferences) implementation(libs.androidx.datastore.datastore.preferences)
implementation(libs.com.squareup.moshi.moshi.kotlin)
implementation(libs.com.github.sadellie.themmo.core) implementation(libs.com.github.sadellie.themmo.core)
ksp(libs.com.squareup.moshi.moshi.kotlin.codegen)
implementation(project(":data:database")) implementation(project(":data:database"))
implementation(project(":data:model")) implementation(project(":data:model"))

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev * Copyright (c) 2023-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,22 +18,8 @@
package com.sadellie.unitto.data.backup 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.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.sadellie.unitto.data.database.CalculatorHistoryEntity
import com.sadellie.unitto.data.database.TimeZoneEntity
import com.sadellie.unitto.data.database.UnitsEntity
import com.sadellie.unitto.data.userprefs.PrefsKeys
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
/** /**
@ -43,130 +29,5 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class BackupManagerTest { class BackupManagerTest {
// TODO Write tests. Currently testing manually.
private lateinit var dataStore: DataStore<Preferences>
private lateinit var backupManager: BackupManager
@Before
fun setup() {
// Inserting dummy data as app data (db and prefs)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
dataStore = PreferenceDataStoreFactory.create(
corruptionHandler = null,
produceFile = { appContext.preferencesDataStoreFile("test") }
)
backupManager = BackupManager(
mContext = appContext,
dataStore = dataStore,
unitsDao = FakeUnitsDao,
timeZoneDao = FakeTimeZoneDao,
calculatorHistoryDao = FakeCalculatorHistoryDao
)
runBlocking {
dataStore.edit {
it[PrefsKeys.THEMING_MODE] = fakeUserData.themingMode
it[PrefsKeys.ENABLE_DYNAMIC_THEME] = fakeUserData.enableDynamicTheme
it[PrefsKeys.ENABLE_AMOLED_THEME] = fakeUserData.enableAmoledTheme
it[PrefsKeys.CUSTOM_COLOR] = fakeUserData.customColor
it[PrefsKeys.MONET_MODE] = fakeUserData.monetMode
it[PrefsKeys.STARTING_SCREEN] = fakeUserData.startingScreen
it[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] = fakeUserData.enableToolsExperiment
it[PrefsKeys.SYSTEM_FONT] = fakeUserData.systemFont
it[PrefsKeys.LAST_READ_CHANGELOG] = fakeUserData.lastReadChangelog
it[PrefsKeys.ENABLE_VIBRATIONS] = fakeUserData.enableVibrations
it[PrefsKeys.MIDDLE_ZERO] = fakeUserData.middleZero
it[PrefsKeys.AC_BUTTON] = fakeUserData.acButton
it[PrefsKeys.RPN_MODE] = fakeUserData.rpnMode
it[PrefsKeys.DIGITS_PRECISION] = fakeUserData.precision
it[PrefsKeys.SEPARATOR] = fakeUserData.separator
it[PrefsKeys.OUTPUT_FORMAT] = fakeUserData.outputFormat
it[PrefsKeys.RADIAN_MODE] = fakeUserData.radianMode
it[PrefsKeys.PARTIAL_HISTORY_VIEW] = fakeUserData.partialHistoryView
it[PrefsKeys.LATEST_LEFT_SIDE] = fakeUserData.latestLeftSide
it[PrefsKeys.LATEST_RIGHT_SIDE] = fakeUserData.latestRightSide
it[PrefsKeys.SHOWN_UNIT_GROUPS] = fakeUserData.shownUnitGroups
it[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] = fakeUserData.unitConverterFavoritesOnly
it[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] = fakeUserData.unitConverterFormatTime
it[PrefsKeys.UNIT_CONVERTER_SORTING] = fakeUserData.unitConverterSorting
}
}
}
@Test
fun testBackup() = runBlocking{
// Backup manager also saves the file to disk, but it is not tested here.
// There is probably no need to test if the data in app is valid since moshi will throw an
// exceptions if the data in app is invalid. For example, if unitConverterSorting is set to
// "Qwerty" (this sorting doesn't exist) there will be an exception.
val actualUserData = backupManager.userDataFromApp()
val expectedUserData = fakeUserData
assertEquals(expectedUserData, actualUserData)
}
@Test
fun testRestoreDatastore() = runBlocking{
// Backup manager also saves the file to disk, but it is not tested here.
// There is probably no need to test if the data in app is valid since moshi will throw an
// exceptions if the data in app is invalid. For example, if unitConverterSorting is set to
// "Qwerty" (this sorting doesn't exist) there will be an exception.
backupManager.updateDatastore(fakeUserData)
assertEquals(backupManager.userDataFromApp(), fakeUserData)
}
@Test
fun testRestoreCalculatorHistoryTable() = runBlocking {
// Data for import
val fakeUserData2 = fakeUserData.copy(
calculatorHistoryTable = listOf(
CalculatorHistoryEntity(
timestamp = System.currentTimeMillis(),
expression = "69+420",
result = "444"
)
)
)
backupManager.updateCalculatorHistoryTable(fakeUserData2)
assertEquals(FakeCalculatorHistoryDao.getAllDescending().first(), fakeUserData2.calculatorHistoryTable)
}
@Test
fun testRestoreUnitsTable() = runBlocking {
// Data for import
val fakeUserData2 = fakeUserData.copy(
unitsTable = listOf(
UnitsEntity(
unitId = "UnitId3",
isFavorite = false,
pairedUnitId = "Pair4",
frequency = 123
)
)
)
backupManager.updateUnitsTable(fakeUserData2)
assertEquals(FakeUnitsDao.getAllFlow().first(), fakeUserData2.unitsTable)
}
@Test
fun testRestoreTimeZonesTable() = runBlocking {
// Data for import
val fakeUserData2 = fakeUserData.copy(
timeZoneTable = listOf(
TimeZoneEntity(
id = "id3",
position = 123,
label = "label456"
)
)
)
backupManager.updateTimeZonesTable(fakeUserData2)
assertEquals(FakeTimeZoneDao.getFavorites().first(), fakeUserData2.timeZoneTable)
}
} }

View File

@ -1,46 +0,0 @@
/*
* 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.CalculatorHistoryDao
import com.sadellie.unitto.data.database.CalculatorHistoryEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
var calculatorHistory: List<CalculatorHistoryEntity> = listOf(
CalculatorHistoryEntity(
timestamp = System.currentTimeMillis(),
expression = "2+2",
result = "4"
)
)
object FakeCalculatorHistoryDao: CalculatorHistoryDao {
override fun getAllDescending(): Flow<List<CalculatorHistoryEntity>> = flow {
emit(calculatorHistory)
}
override suspend fun insert(vararg historyEntity: CalculatorHistoryEntity) {
calculatorHistory += historyEntity
}
override suspend fun clear() {
calculatorHistory = emptyList()
}
}

View File

@ -1,56 +0,0 @@
/*
* 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
var 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>> = flow {
emit(timeZones)
}
override fun getMaxPosition(): Int = 0
override suspend fun insert(vararg timeZoneEntity: TimeZoneEntity) {
timeZones += 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() {
timeZones = emptyList()
}
}

View File

@ -1,55 +0,0 @@
/*
* 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
var 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) {
units += unit
}
override suspend fun getById(unitId: String): UnitsEntity? = null
override suspend fun clear() {
units = emptyList()
}
}

View File

@ -1,49 +0,0 @@
/*
* 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
internal val fakeUserData = UserData(
themingMode = "AUTO",
enableDynamicTheme = false,
enableAmoledTheme = false,
customColor = 777L,
monetMode = "TonalSpot",
startingScreen = "calculator_route",
enableToolsExperiment = false,
systemFont = false,
lastReadChangelog = "33",
enableVibrations = false,
middleZero = false,
acButton = false,
rpnMode = false,
precision = 11,
separator = 1,
outputFormat = 1,
radianMode = false,
partialHistoryView = false,
latestLeftSide = "kilometer",
latestRightSide = "mile",
shownUnitGroups = "LENGTH",
unitConverterFavoritesOnly = false,
unitConverterFormatTime = false,
unitConverterSorting = "USAGE",
calculatorHistoryTable = calculatorHistory,
unitsTable = units,
timeZoneTable = timeZones
)

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
~ Unitto is a unit converter for Android ~ Unitto is a unit converter for Android
~ Copyright (c) 2023 Elshan Agaev ~ Copyright (c) 2023-2024 Elshan Agaev
~ ~
~ This program is free software: you can redistribute it and/or modify ~ 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 ~ it under the terms of the GNU General Public License as published by
@ -17,16 +17,5 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest>
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities=".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> </manifest>

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev * Copyright (c) 2023-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,187 +18,137 @@
package com.sadellie.unitto.data.backup package com.sadellie.unitto.data.backup
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import com.sadellie.unitto.data.database.DATABASE_NAME
import androidx.datastore.core.DataStore import com.sadellie.unitto.data.database.UnittoDatabase
import androidx.datastore.preferences.core.Preferences import com.sadellie.unitto.data.database.checkpoint
import androidx.datastore.preferences.core.edit import com.sadellie.unitto.data.userprefs.USER_PREFERENCES
import com.sadellie.unitto.data.database.CalculatorHistoryDao
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.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.getLastReadChangelog
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.getSystemFont
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.adapter
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.InputStreamReader import java.util.zip.ZipEntry
import javax.inject.Inject import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@OptIn(ExperimentalStdlibApi::class) class BackupManager {
class BackupManager @Inject constructor( suspend fun backup(
@ApplicationContext private val mContext: Context, context: Context,
private val dataStore: DataStore<Preferences>, backupFileUri: Uri,
private val calculatorHistoryDao: CalculatorHistoryDao, database: UnittoDatabase,
private val unitsDao: UnitsDao, ) = withContext(Dispatchers.IO) {
private val timeZoneDao: TimeZoneDao, context
) { .applicationContext
private val moshi: Moshi = Moshi.Builder() .contentResolver
.add(UserDataTableAdapter()) .openOutputStream(backupFileUri)
.build() ?.use { backupFileOutputStream ->
private val jsonAdapter: JsonAdapter<UserData> = moshi.adapter<UserData>() ZipOutputStream(backupFileOutputStream.buffered())
private val auth = "com.sadellie.unitto.BackupManager" .use { zipOutputStream ->
// Datastore
context
.datastoreFile
.writeToZip(zipOutputStream, DATASTORE_FILE_NAME)
suspend fun backup(): Uri = withContext(Dispatchers.IO) { // Database
val userData = userDataFromApp() database.checkpoint()
// save to disk database
val tempFile = File.createTempFile("backup", ".unitto", mContext.cacheDir) .file
tempFile.writeText(jsonAdapter.toJson(userData)) .writeToZip(zipOutputStream, DATABASE_FILE_NAME)
}
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
updateCalculatorHistoryTable(userData)
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 { suspend fun restore(
val data = dataStore.data.first() context: Context,
val calculatorHistoryTable = calculatorHistoryDao.getAllDescending().first() backupFileUri: Uri,
val unitsTableData = unitsDao.getAllFlow().first() database: UnittoDatabase,
val timeZoneTableData = timeZoneDao.getFavorites().first() ) = withContext(Dispatchers.IO) {
context
.applicationContext
.contentResolver
.openInputStream(backupFileUri)
?.use { backupFileInputStream ->
ZipInputStream(backupFileInputStream).use { zipInputStream ->
var entry = zipInputStream.nextEntry
while (entry != null) {
when (entry.name) {
DATASTORE_FILE_NAME -> {
context
.datastoreFile
.writeFromZip(zipInputStream)
}
return UserData( DATABASE_FILE_NAME -> {
themingMode = data.getThemingMode().name, database.checkpoint()
enableDynamicTheme = data.getEnableDynamicTheme(), database.close()
enableAmoledTheme = data.getEnableAmoledTheme(), database
customColor = data.getCustomColor(), .file
monetMode = data.getMonetMode().name, .writeFromZip(zipInputStream)
startingScreen = data.getStartingScreen(), }
enableToolsExperiment = data.getEnableToolsExperiment(), }
systemFont = data.getSystemFont(), entry = zipInputStream.nextEntry
lastReadChangelog = data.getLastReadChangelog(), }
enableVibrations = data.getEnableVibrations(), }
middleZero = data.getMiddleZero(), } ?: return@withContext // Don't restart activity if the file is not found
acButton = data.getAcButton(),
rpnMode = data.getRpnMode(),
precision = data.getDigitsPrecision(),
separator = data.getSeparator(),
outputFormat = data.getOutputFormat(),
radianMode = data.getRadianMode(),
partialHistoryView = data.getPartialHistoryView(),
latestLeftSide = data.getLatestLeftSide(),
latestRightSide = data.getLatestRightSide(),
shownUnitGroups = data.getShownUnitGroups().joinToString(","),
unitConverterFavoritesOnly = data.getUnitConverterFavoritesOnly(),
unitConverterFormatTime = data.getUnitConverterFormatTime(),
unitConverterSorting = data.getUnitConverterSorting().name,
calculatorHistoryTable = calculatorHistoryTable,
unitsTable = unitsTableData,
timeZoneTable = timeZoneTableData,
)
}
internal suspend fun updateDatastore(userData: UserData) { context.restartActivity()
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.SYSTEM_FONT] = userData.systemFont
it[PrefsKeys.LAST_READ_CHANGELOG] = userData.lastReadChangelog
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
// UNIT CONVERTER
it[PrefsKeys.LATEST_LEFT_SIDE] = userData.latestLeftSide
it[PrefsKeys.LATEST_RIGHT_SIDE] = userData.latestRightSide
it[PrefsKeys.SHOWN_UNIT_GROUPS] = userData.shownUnitGroups
it[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] = userData.unitConverterFavoritesOnly
it[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] = userData.unitConverterFormatTime
it[PrefsKeys.UNIT_CONVERTER_SORTING] = userData.unitConverterSorting
}
}
internal suspend fun updateCalculatorHistoryTable(userData: UserData) {
calculatorHistoryDao.clear()
userData.calculatorHistoryTable.forEach { calculatorHistoryDao.insert(it) }
}
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) }
} }
} }
private val Context.datastoreFile: File
get() = this
.filesDir
.addChild(DATASTORE_FOLDER_NAME)
.addChild(DATASTORE_FILE_NAME)
private fun Context.restartActivity() {
val componentName: ComponentName = this
.packageManager
.getLaunchIntentForPackage(this.packageName)
?.component
?: throw Exception("BackupManager was unable to find component for this context")
val restartIntent: Intent = Intent.makeRestartActivityTask(componentName)
this.startActivity(restartIntent)
Runtime.getRuntime().exit(0)
}
private val UnittoDatabase.file: File
get() = File(this.openHelper.writableDatabase.path!!) // Will be caught on higher level if null
/**
* Will write content of this [File] into given [ZipOutputStream].
*
* @receiver Source [File].
* @param zipOutputStream Target [ZipOutputStream].
* @param name Name of the file ([ZipEntry]) that will be created in the archive [ZipOutputStream].
*/
private fun File.writeToZip(zipOutputStream: ZipOutputStream, name: String) = this
.inputStream()
.buffered()
.use { inputStream ->
// Using explicit names only to reduce overhead. Don't use this.name,
zipOutputStream.putNextEntry(ZipEntry(name))
inputStream.copyTo(zipOutputStream)
}
/**
* Will write content from given [ZipInputStream] into [File].
*
* @receiver Target [File] that will be filled with content from [ZipInputStream].
* @param zipInputStream Source [ZipInputStream].
*/
private fun File.writeFromZip(zipInputStream: ZipInputStream) = this
.outputStream()
.buffered()
.use { outputStream ->
zipInputStream.copyTo(outputStream)
}
private fun File.addChild(child: String): File = File(this, child)
private const val DATASTORE_FILE_NAME = "$USER_PREFERENCES.preferences_pb"
private const val DATASTORE_FOLDER_NAME = "datastore"
private const val DATABASE_FILE_NAME = "$DATABASE_NAME.db"

View File

@ -1,61 +0,0 @@
/*
* 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.CalculatorHistoryEntity
import com.sadellie.unitto.data.database.TimeZoneEntity
import com.sadellie.unitto.data.database.UnitsEntity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// Don't move to model module. This uses entity classes from database module
@JsonClass(generateAdapter = true)
internal data class UserData(
@Json(name = "themingMode") val themingMode: String,
@Json(name = "enableDynamicTheme") val enableDynamicTheme: Boolean,
@Json(name = "enableAmoledTheme") val enableAmoledTheme: Boolean,
@Json(name = "customColor") val customColor: Long,
@Json(name = "monetMode") val monetMode: String,
@Json(name = "startingScreen") val startingScreen: String,
@Json(name = "enableToolsExperiment") val enableToolsExperiment: Boolean,
@Json(name = "systemFont") val systemFont: Boolean,
@Json(name = "lastReadChangelog") val lastReadChangelog: String,
@Json(name = "enableVibrations") val enableVibrations: Boolean,
@Json(name = "middleZero") val middleZero: Boolean,
@Json(name = "acButton") val acButton: Boolean,
@Json(name = "rpnMode") val rpnMode: Boolean,
@Json(name = "precision") val precision: Int,
@Json(name = "separator") val separator: Int,
@Json(name = "outputFormat") val outputFormat: Int,
@Json(name = "radianMode") val radianMode: Boolean,
@Json(name = "partialHistoryView") val partialHistoryView: Boolean,
@Json(name = "latestLeftSide") val latestLeftSide: String,
@Json(name = "latestRightSide") val latestRightSide: String,
@Json(name = "shownUnitGroups") val shownUnitGroups: String,
@Json(name = "unitConverterFavoritesOnly") val unitConverterFavoritesOnly: Boolean,
@Json(name = "unitConverterFormatTime") val unitConverterFormatTime: Boolean,
@Json(name = "unitConverterSorting") val unitConverterSorting: String,
@Json(name = "calculatorHistoryTable") val calculatorHistoryTable: List<CalculatorHistoryEntity>,
@Json(name = "unitsTable") val unitsTable: List<UnitsEntity>,
@Json(name = "timeZoneTable") val timeZoneTable: List<TimeZoneEntity>,
)

View File

@ -1,81 +0,0 @@
/*
* 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.CalculatorHistoryEntity
import com.sadellie.unitto.data.database.TimeZoneEntity
import com.sadellie.unitto.data.database.UnitsEntity
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
// Have to use this wrapper since entity classes are in database module
@Suppress("UNUSED")
internal class UserDataTableAdapter {
@ToJson
fun toJson(calculatorHistoryEntity: CalculatorHistoryEntity): UserDataCalculatorHistory =
UserDataCalculatorHistory(
entityId = calculatorHistoryEntity.entityId,
timestamp = calculatorHistoryEntity.timestamp,
expression = calculatorHistoryEntity.expression,
result = calculatorHistoryEntity.result
)
@FromJson
fun fromJson(userDataCalculatorHistory: UserDataCalculatorHistory): CalculatorHistoryEntity =
CalculatorHistoryEntity(
entityId = userDataCalculatorHistory.entityId,
timestamp = userDataCalculatorHistory.timestamp,
expression = userDataCalculatorHistory.expression,
result = userDataCalculatorHistory.result
)
@ToJson
fun toJson(unitsEntity: UnitsEntity): UserDataUnit =
UserDataUnit(
unitId = unitsEntity.unitId,
isFavorite = unitsEntity.isFavorite,
pairedUnitId = unitsEntity.pairedUnitId,
frequency = unitsEntity.frequency
)
@FromJson
fun fromJson(userDataUnit: UserDataUnit): UnitsEntity =
UnitsEntity(
unitId = userDataUnit.unitId,
isFavorite = userDataUnit.isFavorite,
pairedUnitId = userDataUnit.pairedUnitId,
frequency = userDataUnit.frequency
)
@ToJson
fun toJson(timeZoneEntity: TimeZoneEntity): UserDataTimezone =
UserDataTimezone(
id = timeZoneEntity.id,
position = timeZoneEntity.position,
label = timeZoneEntity.label
)
@FromJson
fun fromJson(userDataTimezone: UserDataTimezone): TimeZoneEntity =
TimeZoneEntity(
id = userDataTimezone.id,
position = userDataTimezone.position,
label = userDataTimezone.label
)
}

View File

@ -1,28 +0,0 @@
/*
* 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.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserDataTimezone(
val id: String,
val position: Int,
val label: String,
)

View File

@ -1,29 +0,0 @@
/*
* 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.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserDataUnit(
val unitId: String,
val isFavorite: Boolean,
val pairedUnitId: String?,
val frequency: Int,
)

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev * Copyright (c) 2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -16,14 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.sadellie.unitto.data.backup package com.sadellie.unitto.data.database
import com.squareup.moshi.JsonClass import androidx.room.Dao
import androidx.room.RawQuery
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
@JsonClass(generateAdapter = true) @Dao
internal data class UserDataCalculatorHistory( interface RawDao {
val entityId: Int, @RawQuery
val timestamp: Long, suspend fun execute(query: SupportSQLiteQuery): Int
val expression: String,
val result: String, suspend fun walCheckpoint() = execute(SimpleSQLiteQuery("PRAGMA wal_checkpoint(FULL)"))
) }

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2022-2023 Elshan Agaev * Copyright (c) 2022-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -46,4 +46,7 @@ abstract class UnittoDatabase : RoomDatabase() {
abstract fun calculatorHistoryDao(): CalculatorHistoryDao abstract fun calculatorHistoryDao(): CalculatorHistoryDao
abstract fun timeZoneDao(): TimeZoneDao abstract fun timeZoneDao(): TimeZoneDao
abstract fun currencyRatesDao(): CurrencyRatesDao abstract fun currencyRatesDao(): CurrencyRatesDao
internal abstract fun rawDao(): RawDao
} }
suspend fun UnittoDatabase.checkpoint() = this.rawDao().walCheckpoint()

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2022-2023 Elshan Agaev * Copyright (c) 2022-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -27,6 +27,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
const val DATABASE_NAME = "unitto_database"
/** /**
* Module for database. Used to access same instance of database * Module for database. Used to access same instance of database
* *
@ -34,6 +36,10 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@Module @Module
class UnittoDatabaseModule { class UnittoDatabaseModule {
@Provides
fun provideRawDao(unittoDatabase: UnittoDatabase): RawDao {
return unittoDatabase.rawDao()
}
@Provides @Provides
fun provideUnitsDao(unittoDatabase: UnittoDatabase): UnitsDao { fun provideUnitsDao(unittoDatabase: UnittoDatabase): UnitsDao {
@ -73,7 +79,7 @@ class UnittoDatabaseModule {
return Room.databaseBuilder( return Room.databaseBuilder(
appContext.applicationContext, appContext.applicationContext,
UnittoDatabase::class.java, UnittoDatabase::class.java,
"unitto_database" DATABASE_NAME
).build() ).build()
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2022-2023 Elshan Agaev * Copyright (c) 2022-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -34,7 +34,7 @@ import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
// DON'T TOUCH!!! // DON'T TOUCH!!!
private const val USER_PREFERENCES = "settings" const val USER_PREFERENCES = "settings"
/** /**
* This module is for DataStore dependency injection * This module is for DataStore dependency injection

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2022-2023 Elshan Agaev * Copyright (c) 2022-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -20,13 +20,11 @@ package com.sadellie.unitto.feature.settings
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
@ -93,6 +91,8 @@ import com.sadellie.unitto.feature.settings.navigation.formattingRoute
import com.sadellie.unitto.feature.settings.navigation.startingScreenRoute import com.sadellie.unitto.feature.settings.navigation.startingScreenRoute
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@Composable @Composable
internal fun SettingsRoute( internal fun SettingsRoute(
@ -101,21 +101,13 @@ internal fun SettingsRoute(
navControllerAction: (String) -> Unit, navControllerAction: (String) -> Unit,
) { ) {
val mContext = LocalContext.current val mContext = LocalContext.current
val uiState: SettingsUIState = viewModel.uiState
val errorLabel = stringResource(R.string.error_label) .collectAsStateWithLifecycle().value
val showErrorToast: Boolean = viewModel.showErrorToast
val uiState: SettingsUIState = viewModel.uiState.collectAsStateWithLifecycle().value .collectAsStateWithLifecycle(initialValue = false).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) { LaunchedEffect(showErrorToast) {
if (showErrorToast) Toast.makeText(mContext, errorLabel, Toast.LENGTH_SHORT).show() if (showErrorToast) showToast(mContext, mContext.resources.getString(R.string.error_label))
} }
when (uiState) { when (uiState) {
@ -142,15 +134,24 @@ private fun SettingsScreen(
updateLastReadChangelog: (String) -> Unit, updateLastReadChangelog: (String) -> Unit,
updateVibrations: (Boolean) -> Unit, updateVibrations: (Boolean) -> Unit,
clearCache: () -> Unit, clearCache: () -> Unit,
backup: () -> Unit, backup: (Context, Uri) -> Unit,
restore: (Uri) -> Unit = {}, restore: (Context, Uri) -> Unit,
) { ) {
val mContext = LocalContext.current val mContext = LocalContext.current
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
// Pass picked file uri to BackupManager // Pass picked file uri to BackupManager
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { pickedUri -> val restoreLauncher = rememberLauncherForActivityResult(
if (pickedUri != null) restore(pickedUri) ActivityResultContracts.OpenDocument()
) { pickedUri ->
if (pickedUri != null) restore(mContext, pickedUri)
}
// Pass picked file uri to BackupManager
val backupLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument(backupMimeType)
) { pickedUri ->
if (pickedUri != null) backup(mContext, pickedUri)
} }
BackHandler(uiState.backupInProgress) {} BackHandler(uiState.backupInProgress) {}
@ -168,11 +169,17 @@ private fun SettingsScreen(
onDismissRequest = { showMenu = false } onDismissRequest = { showMenu = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
onClick = { showMenu = false; backup() }, onClick = {
showMenu = false
backupLauncher.launchSafely(backupFileName())
},
text = { Text(stringResource(R.string.settings_back_up)) } text = { Text(stringResource(R.string.settings_back_up)) }
) )
DropdownMenuItem( DropdownMenuItem(
onClick = { showMenu = false; launcher.launchPicker() }, onClick = {
showMenu = false
restoreLauncher.launchSafely(arrayOf(backupMimeType))
},
text = { Text(stringResource(R.string.settings_restore)) } text = { Text(stringResource(R.string.settings_restore)) }
) )
} }
@ -297,25 +304,20 @@ private fun SettingsScreen(
} }
} }
private fun Context.share(uri: Uri) { private fun <T> ActivityResultLauncher<T>.launchSafely(input: T) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = backupMimeType
}
startActivity(shareIntent)
}
private fun ManagedActivityResultLauncher<Array<String>, Uri?>.launchPicker() {
try { try {
launch(arrayOf(backupMimeType)) this.launch(input)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e("SettingsScreen", "launchPicker: ActivityNotFoundException") Log.e("SettingsScreen", "launchSafely: ActivityNotFoundException")
} }
} }
private const val backupMimeType = "application/octet-stream" private fun backupFileName(): String {
val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
return "${ZonedDateTime.now().format(formatter)}.zip"
}
private const val backupMimeType = "application/zip"
@Preview @Preview
@Composable @Composable
@ -348,7 +350,14 @@ private fun PreviewSettingsScreen() {
clearCache = { clearCache = {
uiState = uiState.copy(cacheSize = 0) uiState = uiState.copy(cacheSize = 0)
}, },
backup = { backup = { _, _ ->
corScope.launch {
uiState = uiState.copy(backupInProgress = true)
delay(2000)
uiState = uiState.copy(backupInProgress = false)
}
},
restore = { _, _ ->
corScope.launch { corScope.launch {
uiState = uiState.copy(backupInProgress = true) uiState = uiState.copy(backupInProgress = true)
delay(2000) delay(2000)

View File

@ -1,6 +1,6 @@
/* /*
* Unitto is a unit converter for Android * Unitto is a unit converter for Android
* Copyright (c) 2022-2023 Elshan Agaev * Copyright (c) 2022-2024 Elshan Agaev
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,6 +18,7 @@
package com.sadellie.unitto.feature.settings package com.sadellie.unitto.feature.settings
import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -26,6 +27,7 @@ import com.sadellie.unitto.core.base.BuildConfig
import com.sadellie.unitto.data.backup.BackupManager import com.sadellie.unitto.data.backup.BackupManager
import com.sadellie.unitto.data.common.stateIn import com.sadellie.unitto.data.common.stateIn
import com.sadellie.unitto.data.database.CurrencyRatesDao import com.sadellie.unitto.data.database.CurrencyRatesDao
import com.sadellie.unitto.data.database.UnittoDatabase
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -42,11 +44,8 @@ import javax.inject.Inject
internal class SettingsViewModel @Inject constructor( internal class SettingsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository, private val userPrefsRepository: UserPreferencesRepository,
private val currencyRatesDao: CurrencyRatesDao, private val currencyRatesDao: CurrencyRatesDao,
private val backupManager: BackupManager private val database: UnittoDatabase
) : ViewModel() { ) : ViewModel() {
private val _backupFileUri = MutableSharedFlow<Uri?>()
val backupFileUri = _backupFileUri.asSharedFlow()
private val _showErrorToast = MutableSharedFlow<Boolean>() private val _showErrorToast = MutableSharedFlow<Boolean>()
val showErrorToast = _showErrorToast.asSharedFlow() val showErrorToast = _showErrorToast.asSharedFlow()
@ -67,14 +66,15 @@ internal class SettingsViewModel @Inject constructor(
} }
.stateIn(viewModelScope, SettingsUIState.Loading) .stateIn(viewModelScope, SettingsUIState.Loading)
fun backup() { fun backup(
context: Context,
uri: Uri,
) {
backupJob?.cancel() backupJob?.cancel()
backupJob = viewModelScope.launch(Dispatchers.IO) { backupJob = viewModelScope.launch(Dispatchers.IO) {
_backupInProgress.update { true } _backupInProgress.update { true }
try { try {
val backupFileUri = backupManager.backup() BackupManager().backup(context, uri, database)
_backupFileUri.emit(backupFileUri) // Emit to trigger file share intent
_showErrorToast.emit(false)
} catch (e: Exception) { } catch (e: Exception) {
_showErrorToast.emit(true) _showErrorToast.emit(true)
Log.e(TAG, "$e") Log.e(TAG, "$e")
@ -83,13 +83,15 @@ internal class SettingsViewModel @Inject constructor(
} }
} }
fun restore(uri: Uri) { fun restore(
context: Context,
uri: Uri,
) {
backupJob?.cancel() backupJob?.cancel()
backupJob = viewModelScope.launch(Dispatchers.IO) { backupJob = viewModelScope.launch(Dispatchers.IO) {
_backupInProgress.update { true } _backupInProgress.update { true }
try { try {
backupManager.restore(uri) BackupManager().restore(context, uri, database)
_showErrorToast.emit(false)
} catch (e: Exception) { } catch (e: Exception) {
_showErrorToast.emit(true) _showErrorToast.emit(true)
Log.e(TAG, "$e") Log.e(TAG, "$e")

View File

@ -76,7 +76,6 @@ com-google-dagger-android-hilt-android = { group = "com.google.dagger", name = "
com-google-dagger-dagger-android-processor = { group = "com.google.dagger", name = "dagger-android-processor", version.ref = "comGoogleDagger" } com-google-dagger-dagger-android-processor = { group = "com.google.dagger", name = "dagger-android-processor", version.ref = "comGoogleDagger" }
com-google-dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "comGoogleDagger" } com-google-dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "comGoogleDagger" }
com-squareup-moshi-moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "comSquareupMoshiMoshiKotlin" } com-squareup-moshi-moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "comSquareupMoshiMoshiKotlin" }
com-squareup-moshi-moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "comSquareupMoshiMoshiKotlin" }
com-squareup-retrofit2-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "comSquareupRetrofit2ConverterMoshi" } com-squareup-retrofit2-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "comSquareupRetrofit2ConverterMoshi" }
junit-junit = { group = "junit", name = "junit", version.ref = "junitJunit" } junit-junit = { group = "junit", name = "junit", version.ref = "junitJunit" }
org-burnoutcrew-composereorderable-reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "orgBurnoutcrewComposereorderableReorderable" } org-burnoutcrew-composereorderable-reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "orgBurnoutcrewComposereorderableReorderable" }