mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
Rewrite BackupManager
This commit is contained in:
parent
d9454d8d01
commit
8d46084b51
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -17,19 +17,25 @@
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("com.google.devtools.ksp")
|
||||
id("unitto.library")
|
||||
id("unitto.android.library.jacoco")
|
||||
id("unitto.android.hilt")
|
||||
id("unitto.room")
|
||||
id("unitto.android.library.jacoco")
|
||||
}
|
||||
|
||||
android.namespace = "com.sadellie.unitto.data.backup"
|
||||
|
||||
android {
|
||||
room {
|
||||
val schemaLocation = "$projectDir/schemas"
|
||||
schemaDirectory(schemaLocation)
|
||||
println("Exported Database schema to $schemaLocation")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.datastore.datastore.preferences)
|
||||
implementation(libs.com.squareup.moshi.moshi.kotlin)
|
||||
implementation(libs.com.github.sadellie.themmo.core)
|
||||
ksp(libs.com.squareup.moshi.moshi.kotlin.codegen)
|
||||
|
||||
implementation(project(":data:database"))
|
||||
implementation(project(":data:model"))
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -18,22 +18,8 @@
|
||||
|
||||
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.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.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
@ -43,130 +29,5 @@ import org.junit.runner.RunWith
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BackupManagerTest {
|
||||
|
||||
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)
|
||||
}
|
||||
// TODO Write tests. Currently testing manually.
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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
|
||||
~ 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/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -18,187 +18,137 @@
|
||||
|
||||
package com.sadellie.unitto.data.backup
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.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 com.sadellie.unitto.data.database.DATABASE_NAME
|
||||
import com.sadellie.unitto.data.database.UnittoDatabase
|
||||
import com.sadellie.unitto.data.database.checkpoint
|
||||
import com.sadellie.unitto.data.userprefs.USER_PREFERENCES
|
||||
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
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class BackupManager @Inject constructor(
|
||||
@ApplicationContext private val mContext: Context,
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
private val calculatorHistoryDao: CalculatorHistoryDao,
|
||||
private val unitsDao: UnitsDao,
|
||||
private val timeZoneDao: TimeZoneDao,
|
||||
) {
|
||||
private val moshi: Moshi = Moshi.Builder()
|
||||
.add(UserDataTableAdapter())
|
||||
.build()
|
||||
private val jsonAdapter: JsonAdapter<UserData> = moshi.adapter<UserData>()
|
||||
private val auth = "com.sadellie.unitto.BackupManager"
|
||||
class BackupManager {
|
||||
suspend fun backup(
|
||||
context: Context,
|
||||
backupFileUri: Uri,
|
||||
database: UnittoDatabase,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
context
|
||||
.applicationContext
|
||||
.contentResolver
|
||||
.openOutputStream(backupFileUri)
|
||||
?.use { backupFileOutputStream ->
|
||||
ZipOutputStream(backupFileOutputStream.buffered())
|
||||
.use { zipOutputStream ->
|
||||
// Datastore
|
||||
context
|
||||
.datastoreFile
|
||||
.writeToZip(zipOutputStream, DATASTORE_FILE_NAME)
|
||||
|
||||
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()
|
||||
}
|
||||
// Database
|
||||
database.checkpoint()
|
||||
database
|
||||
.file
|
||||
.writeToZip(zipOutputStream, DATABASE_FILE_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
val data = dataStore.data.first()
|
||||
val calculatorHistoryTable = calculatorHistoryDao.getAllDescending().first()
|
||||
val unitsTableData = unitsDao.getAllFlow().first()
|
||||
val timeZoneTableData = timeZoneDao.getFavorites().first()
|
||||
suspend fun restore(
|
||||
context: Context,
|
||||
backupFileUri: Uri,
|
||||
database: UnittoDatabase,
|
||||
) = 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(
|
||||
themingMode = data.getThemingMode().name,
|
||||
enableDynamicTheme = data.getEnableDynamicTheme(),
|
||||
enableAmoledTheme = data.getEnableAmoledTheme(),
|
||||
customColor = data.getCustomColor(),
|
||||
monetMode = data.getMonetMode().name,
|
||||
startingScreen = data.getStartingScreen(),
|
||||
enableToolsExperiment = data.getEnableToolsExperiment(),
|
||||
systemFont = data.getSystemFont(),
|
||||
lastReadChangelog = data.getLastReadChangelog(),
|
||||
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(),
|
||||
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,
|
||||
)
|
||||
}
|
||||
DATABASE_FILE_NAME -> {
|
||||
database.checkpoint()
|
||||
database.close()
|
||||
database
|
||||
.file
|
||||
.writeFromZip(zipInputStream)
|
||||
}
|
||||
}
|
||||
entry = zipInputStream.nextEntry
|
||||
}
|
||||
}
|
||||
} ?: return@withContext // Don't restart activity if the file is not found
|
||||
|
||||
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.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) }
|
||||
context.restartActivity()
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
@ -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>,
|
||||
)
|
@ -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
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
internal data class UserDataCalculatorHistory(
|
||||
val entityId: Int,
|
||||
val timestamp: Long,
|
||||
val expression: String,
|
||||
val result: String,
|
||||
)
|
||||
@Dao
|
||||
interface RawDao {
|
||||
@RawQuery
|
||||
suspend fun execute(query: SupportSQLiteQuery): Int
|
||||
|
||||
suspend fun walCheckpoint() = execute(SimpleSQLiteQuery("PRAGMA wal_checkpoint(FULL)"))
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* 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 timeZoneDao(): TimeZoneDao
|
||||
abstract fun currencyRatesDao(): CurrencyRatesDao
|
||||
internal abstract fun rawDao(): RawDao
|
||||
}
|
||||
|
||||
suspend fun UnittoDatabase.checkpoint() = this.rawDao().walCheckpoint()
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* 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 javax.inject.Singleton
|
||||
|
||||
const val DATABASE_NAME = "unitto_database"
|
||||
|
||||
/**
|
||||
* Module for database. Used to access same instance of database
|
||||
*
|
||||
@ -34,6 +36,10 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class UnittoDatabaseModule {
|
||||
@Provides
|
||||
fun provideRawDao(unittoDatabase: UnittoDatabase): RawDao {
|
||||
return unittoDatabase.rawDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnitsDao(unittoDatabase: UnittoDatabase): UnitsDao {
|
||||
@ -73,7 +79,7 @@ class UnittoDatabaseModule {
|
||||
return Room.databaseBuilder(
|
||||
appContext.applicationContext,
|
||||
UnittoDatabase::class.java,
|
||||
"unitto_database"
|
||||
DATABASE_NAME
|
||||
).build()
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* 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
|
||||
|
||||
// DON'T TOUCH!!!
|
||||
private const val USER_PREFERENCES = "settings"
|
||||
const val USER_PREFERENCES = "settings"
|
||||
|
||||
/**
|
||||
* This module is for DataStore dependency injection
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* 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.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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 kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
internal fun SettingsRoute(
|
||||
@ -101,21 +101,13 @@ internal fun SettingsRoute(
|
||||
navControllerAction: (String) -> Unit,
|
||||
) {
|
||||
val mContext = LocalContext.current
|
||||
|
||||
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)
|
||||
}
|
||||
val uiState: SettingsUIState = viewModel.uiState
|
||||
.collectAsStateWithLifecycle().value
|
||||
val showErrorToast: Boolean = viewModel.showErrorToast
|
||||
.collectAsStateWithLifecycle(initialValue = false).value
|
||||
|
||||
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) {
|
||||
@ -142,15 +134,24 @@ private fun SettingsScreen(
|
||||
updateLastReadChangelog: (String) -> Unit,
|
||||
updateVibrations: (Boolean) -> Unit,
|
||||
clearCache: () -> Unit,
|
||||
backup: () -> Unit,
|
||||
restore: (Uri) -> Unit = {},
|
||||
backup: (Context, Uri) -> Unit,
|
||||
restore: (Context, 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)
|
||||
val restoreLauncher = rememberLauncherForActivityResult(
|
||||
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) {}
|
||||
@ -168,11 +169,17 @@ private fun SettingsScreen(
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = { showMenu = false; backup() },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
backupLauncher.launchSafely(backupFileName())
|
||||
},
|
||||
text = { Text(stringResource(R.string.settings_back_up)) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = { showMenu = false; launcher.launchPicker() },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
restoreLauncher.launchSafely(arrayOf(backupMimeType))
|
||||
},
|
||||
text = { Text(stringResource(R.string.settings_restore)) }
|
||||
)
|
||||
}
|
||||
@ -297,25 +304,20 @@ private fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(uri: Uri) {
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = backupMimeType
|
||||
}
|
||||
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
|
||||
private fun ManagedActivityResultLauncher<Array<String>, Uri?>.launchPicker() {
|
||||
private fun <T> ActivityResultLauncher<T>.launchSafely(input: T) {
|
||||
try {
|
||||
launch(arrayOf(backupMimeType))
|
||||
this.launch(input)
|
||||
} 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
|
||||
@Composable
|
||||
@ -348,7 +350,14 @@ private fun PreviewSettingsScreen() {
|
||||
clearCache = {
|
||||
uiState = uiState.copy(cacheSize = 0)
|
||||
},
|
||||
backup = {
|
||||
backup = { _, _ ->
|
||||
corScope.launch {
|
||||
uiState = uiState.copy(backupInProgress = true)
|
||||
delay(2000)
|
||||
uiState = uiState.copy(backupInProgress = false)
|
||||
}
|
||||
},
|
||||
restore = { _, _ ->
|
||||
corScope.launch {
|
||||
uiState = uiState.copy(backupInProgress = true)
|
||||
delay(2000)
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.feature.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
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.common.stateIn
|
||||
import com.sadellie.unitto.data.database.CurrencyRatesDao
|
||||
import com.sadellie.unitto.data.database.UnittoDatabase
|
||||
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -42,11 +44,8 @@ import javax.inject.Inject
|
||||
internal class SettingsViewModel @Inject constructor(
|
||||
private val userPrefsRepository: UserPreferencesRepository,
|
||||
private val currencyRatesDao: CurrencyRatesDao,
|
||||
private val backupManager: BackupManager
|
||||
private val database: UnittoDatabase
|
||||
) : ViewModel() {
|
||||
private val _backupFileUri = MutableSharedFlow<Uri?>()
|
||||
val backupFileUri = _backupFileUri.asSharedFlow()
|
||||
|
||||
private val _showErrorToast = MutableSharedFlow<Boolean>()
|
||||
val showErrorToast = _showErrorToast.asSharedFlow()
|
||||
|
||||
@ -67,14 +66,15 @@ internal class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
.stateIn(viewModelScope, SettingsUIState.Loading)
|
||||
|
||||
fun backup() {
|
||||
fun backup(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
) {
|
||||
backupJob?.cancel()
|
||||
backupJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
_backupInProgress.update { true }
|
||||
try {
|
||||
val backupFileUri = backupManager.backup()
|
||||
_backupFileUri.emit(backupFileUri) // Emit to trigger file share intent
|
||||
_showErrorToast.emit(false)
|
||||
BackupManager().backup(context, uri, database)
|
||||
} catch (e: Exception) {
|
||||
_showErrorToast.emit(true)
|
||||
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 = viewModelScope.launch(Dispatchers.IO) {
|
||||
_backupInProgress.update { true }
|
||||
try {
|
||||
backupManager.restore(uri)
|
||||
_showErrorToast.emit(false)
|
||||
BackupManager().restore(context, uri, database)
|
||||
} catch (e: Exception) {
|
||||
_showErrorToast.emit(true)
|
||||
Log.e(TAG, "$e")
|
||||
|
@ -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-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-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" }
|
||||
junit-junit = { group = "junit", name = "junit", version.ref = "junitJunit" }
|
||||
org-burnoutcrew-composereorderable-reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "orgBurnoutcrewComposereorderableReorderable" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user