From 8d46084b512e27142f1a2524c1533d4701a527e2 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Fri, 19 Jan 2024 00:20:53 +0300 Subject: [PATCH] Rewrite BackupManager --- data/backup/build.gradle.kts | 16 +- .../unitto/data/backup/BackupManagerTest.kt | 143 +-------- .../data/backup/FakeCalculatorHistoryDao.kt | 46 --- .../unitto/data/backup/FakeTimeZoneDao.kt | 56 ---- .../unitto/data/backup/FakeUnitsDao.kt | 55 ---- .../backup/FakeUserPreferencesRepository.kt | 49 --- data/backup/src/main/AndroidManifest.xml | 15 +- .../unitto/data/backup/BackupManager.kt | 292 ++++++++---------- .../sadellie/unitto/data/backup/UserData.kt | 61 ---- .../data/backup/UserDataTableAdapter.kt | 81 ----- .../unitto/data/backup/UserDataTimezone.kt | 28 -- .../unitto/data/backup/UserDataUnit.kt | 29 -- .../sadellie/unitto/data/database/RawDao.kt} | 23 +- .../unitto/data/database/UnittoDatabase.kt | 5 +- .../data/database/UnittoDatabaseModule.kt | 10 +- .../unitto/data/userprefs/DataStoreModule.kt | 4 +- .../unitto/feature/settings/SettingsScreen.kt | 85 ++--- .../feature/settings/SettingsViewModel.kt | 26 +- gradle/libs.versions.toml | 1 - 19 files changed, 224 insertions(+), 801 deletions(-) delete mode 100644 data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeCalculatorHistoryDao.kt delete mode 100644 data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt delete mode 100644 data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt delete mode 100644 data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt delete mode 100644 data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt delete mode 100644 data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTableAdapter.kt delete mode 100644 data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTimezone.kt delete mode 100644 data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataUnit.kt rename data/{backup/src/main/java/com/sadellie/unitto/data/backup/UserDataCalculatorHistory.kt => database/src/main/java/com/sadellie/unitto/data/database/RawDao.kt} (63%) diff --git a/data/backup/build.gradle.kts b/data/backup/build.gradle.kts index e52de217..a9ac2303 100644 --- a/data/backup/build.gradle.kts +++ b/data/backup/build.gradle.kts @@ -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")) diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt index d74e2d9f..68ee9a40 100644 --- a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt +++ b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/BackupManagerTest.kt @@ -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 - 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. } diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeCalculatorHistoryDao.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeCalculatorHistoryDao.kt deleted file mode 100644 index e5de78df..00000000 --- a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeCalculatorHistoryDao.kt +++ /dev/null @@ -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 . - */ - -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 = listOf( - CalculatorHistoryEntity( - timestamp = System.currentTimeMillis(), - expression = "2+2", - result = "4" - ) -) - -object FakeCalculatorHistoryDao: CalculatorHistoryDao { - override fun getAllDescending(): Flow> = flow { - emit(calculatorHistory) - } - - override suspend fun insert(vararg historyEntity: CalculatorHistoryEntity) { - calculatorHistory += historyEntity - } - - override suspend fun clear() { - calculatorHistory = emptyList() - } -} diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt deleted file mode 100644 index 25ec3a22..00000000 --- a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeTimeZoneDao.kt +++ /dev/null @@ -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 . - */ - -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> = 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() - } -} \ No newline at end of file diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt deleted file mode 100644 index 9f8ad9a0..00000000 --- a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUnitsDao.kt +++ /dev/null @@ -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 . - */ - -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> { - 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() - } -} \ No newline at end of file diff --git a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt b/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt deleted file mode 100644 index 84f22c96..00000000 --- a/data/backup/src/androidTest/java/com/sadellie/unitto/data/backup/FakeUserPreferencesRepository.kt +++ /dev/null @@ -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 . - */ - -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 -) diff --git a/data/backup/src/main/AndroidManifest.xml b/data/backup/src/main/AndroidManifest.xml index a03d7243..a157463f 100644 --- a/data/backup/src/main/AndroidManifest.xml +++ b/data/backup/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - - - - - - + diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt index d07d4a42..eb6ec295 100644 --- a/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt +++ b/data/backup/src/main/java/com/sadellie/unitto/data/backup/BackupManager.kt @@ -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, - 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 = moshi.adapter() - 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" diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt deleted file mode 100644 index abc7d358..00000000 --- a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserData.kt +++ /dev/null @@ -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 . - */ - -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, - @Json(name = "unitsTable") val unitsTable: List, - @Json(name = "timeZoneTable") val timeZoneTable: List, -) diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTableAdapter.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTableAdapter.kt deleted file mode 100644 index 3f65b1b9..00000000 --- a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTableAdapter.kt +++ /dev/null @@ -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 . - */ - -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 - ) -} diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTimezone.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTimezone.kt deleted file mode 100644 index 92adda53..00000000 --- a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTimezone.kt +++ /dev/null @@ -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 . - */ - -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, -) diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataUnit.kt b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataUnit.kt deleted file mode 100644 index c1fae84d..00000000 --- a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataUnit.kt +++ /dev/null @@ -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 . - */ - -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, -) diff --git a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataCalculatorHistory.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/RawDao.kt similarity index 63% rename from data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataCalculatorHistory.kt rename to data/database/src/main/java/com/sadellie/unitto/data/database/RawDao.kt index 2a0aeaa8..8fb0454a 100644 --- a/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataCalculatorHistory.kt +++ b/data/database/src/main/java/com/sadellie/unitto/data/database/RawDao.kt @@ -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 . */ -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)")) +} diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabase.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabase.kt index 7cbb4bc8..3444492c 100644 --- a/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabase.kt +++ b/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabase.kt @@ -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() diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabaseModule.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabaseModule.kt index c0d5c399..f3811a86 100644 --- a/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabaseModule.kt +++ b/data/database/src/main/java/com/sadellie/unitto/data/database/UnittoDatabaseModule.kt @@ -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() } } \ No newline at end of file diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/DataStoreModule.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/DataStoreModule.kt index 55ce0a1d..3caac528 100644 --- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/DataStoreModule.kt +++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/DataStoreModule.kt @@ -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 diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt index 5299a04d..dd67533d 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt @@ -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, Uri?>.launchPicker() { +private fun ActivityResultLauncher.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) diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt index 955ed3b8..7a4cdadb 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt @@ -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() - val backupFileUri = _backupFileUri.asSharedFlow() - private val _showErrorToast = MutableSharedFlow() 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") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ffcd9634..afdf530b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }