diff --git a/data/backup/consumer-rules.pro b/data/backup/consumer-rules.pro index e69de29b..4f15848f 100644 --- a/data/backup/consumer-rules.pro +++ b/data/backup/consumer-rules.pro @@ -0,0 +1,4 @@ +-keepclassmembers class ** { + @com.squareup.moshi.FromJson *; + @com.squareup.moshi.ToJson *; +} 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 7392bd2e..36086ffc 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 @@ -52,6 +52,7 @@ import com.sadellie.unitto.data.userprefs.getUnitConverterFormatTime import com.sadellie.unitto.data.userprefs.getUnitConverterSorting import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -61,6 +62,7 @@ import java.io.File import java.io.InputStreamReader import javax.inject.Inject +@OptIn(ExperimentalStdlibApi::class) class BackupManager @Inject constructor( @ApplicationContext private val mContext: Context, private val dataStore: DataStore, @@ -69,8 +71,10 @@ class BackupManager @Inject constructor( // Not planned at the moment // private val calculatorHistoryDao: CalculatorHistoryDao, ) { - private val moshi: Moshi = Moshi.Builder().build() - private val jsonAdapter: JsonAdapter = moshi.adapter(UserData::class.java) + private val moshi: Moshi = Moshi.Builder() + .add(UserDataTableAdapter()) + .build() + private val jsonAdapter: JsonAdapter = moshi.adapter() private val auth = "com.sadellie.unitto.BackupManager" suspend fun backup(): Uri = withContext(Dispatchers.IO) { 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 new file mode 100644 index 00000000..522c46c3 --- /dev/null +++ b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTableAdapter.kt @@ -0,0 +1,62 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.data.backup + +import com.sadellie.unitto.data.database.TimeZoneEntity +import com.sadellie.unitto.data.database.UnitsEntity +import com.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(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 new file mode 100644 index 00000000..92adda53 --- /dev/null +++ b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataTimezone.kt @@ -0,0 +1,28 @@ +/* + * 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 new file mode 100644 index 00000000..c1fae84d --- /dev/null +++ b/data/backup/src/main/java/com/sadellie/unitto/data/backup/UserDataUnit.kt @@ -0,0 +1,29 @@ +/* + * 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/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 6ae82b9c..50c1e917 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 @@ -26,7 +26,6 @@ import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -110,41 +109,21 @@ internal fun SettingsRoute( if (showErrorToast) Toast.makeText(mContext, errorLabel, Toast.LENGTH_SHORT).show() } - Crossfade(targetState = uiState) { state -> - when (state) { - SettingsUIState.Loading -> UnittoEmptyScreen() + when (uiState) { + SettingsUIState.Loading -> UnittoEmptyScreen() - SettingsUIState.BackupInProgress -> BackingUpScreen() - - is SettingsUIState.Ready -> SettingsScreen( - uiState = state, - navigateUp = navigateUp, - navControllerAction = navControllerAction, - updateVibrations = viewModel::updateVibrations, - clearCache = viewModel::clearCache, - backup = viewModel::backup, - restore = viewModel::restore - ) - } + is SettingsUIState.Ready -> SettingsScreen( + uiState = uiState, + navigateUp = navigateUp, + navControllerAction = navControllerAction, + updateVibrations = viewModel::updateVibrations, + clearCache = viewModel::clearCache, + backup = viewModel::backup, + restore = viewModel::restore + ) } } -@Composable -private fun BackingUpScreen() { - Scaffold { padding -> - Box( - modifier = Modifier - .padding(padding) - .fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } - - BackHandler {} -} - @Composable private fun SettingsScreen( uiState: SettingsUIState.Ready, @@ -163,6 +142,8 @@ private fun SettingsScreen( if (pickedUri != null) restore(pickedUri) } + BackHandler(uiState.backupInProgress) {} + UnittoScreenWithLargeTopBar( title = stringResource(R.string.settings_title), navigationIcon = { NavigateUpButton(navigateUp) }, @@ -176,11 +157,11 @@ private fun SettingsScreen( onDismissRequest = { showMenu = false } ) { DropdownMenuItem( - onClick = backup, + onClick = { showMenu = false; backup() }, text = { Text("Backup") } ) DropdownMenuItem( - onClick = { launcher.launch(arrayOf(backupMimeType)) }, + onClick = { showMenu = false; launcher.launch(arrayOf(backupMimeType)) }, text = { Text("Restore") } ) } @@ -266,13 +247,26 @@ private fun SettingsScreen( ) } } + + AnimatedVisibility(visible = uiState.backupInProgress) { + Scaffold { padding -> + Box( + modifier = Modifier + .padding(padding) + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + } } private fun Context.share(uri: Uri) { val shareIntent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_STREAM, uri) - type = backupMimeType // This is a fucking war crime, it should be text/json + type = backupMimeType } startActivity(shareIntent) @@ -289,6 +283,7 @@ private fun PreviewSettingsScreen() { uiState = SettingsUIState.Ready( enableVibrations = false, cacheSize = 2, + backupInProgress = false ), navigateUp = {}, navControllerAction = {}, @@ -301,5 +296,16 @@ private fun PreviewSettingsScreen() { @Preview @Composable private fun PreviewBackingUpScreen() { - BackingUpScreen() + SettingsScreen( + uiState = SettingsUIState.Ready( + enableVibrations = false, + cacheSize = 2, + backupInProgress = true + ), + navigateUp = {}, + navControllerAction = {}, + updateVibrations = {}, + clearCache = {}, + backup = {} + ) } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt index fa353bd9..001c9ca9 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsUIState.kt @@ -21,10 +21,9 @@ package com.sadellie.unitto.feature.settings internal sealed class SettingsUIState { data object Loading : SettingsUIState() - data object BackupInProgress : SettingsUIState() - data class Ready( val enableVibrations: Boolean, val cacheSize: Int, + val backupInProgress: Boolean, ) : SettingsUIState() } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt index b82234f4..1aabb9e6 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 @@ -49,19 +49,19 @@ internal class SettingsViewModel @Inject constructor( private val _showErrorToast = MutableSharedFlow() val showErrorToast = _showErrorToast.asSharedFlow() - private val _operation = MutableStateFlow(false) + private val _backupInProgress = MutableStateFlow(false) private var backupJob: Job? = null val uiState = combine( userPrefsRepository.generalPrefs, currencyRatesDao.size(), - _operation, - ) { prefs, cacheSize, operation -> - if (operation) return@combine SettingsUIState.BackupInProgress + _backupInProgress, + ) { prefs, cacheSize, backupInProgress -> SettingsUIState.Ready( enableVibrations = prefs.enableVibrations, cacheSize = cacheSize, + backupInProgress = backupInProgress ) } .stateIn(viewModelScope, SettingsUIState.Loading) @@ -69,7 +69,7 @@ internal class SettingsViewModel @Inject constructor( fun backup() { backupJob?.cancel() backupJob = viewModelScope.launch(Dispatchers.IO) { - _operation.update { true } + _backupInProgress.update { true } try { val backupFileUri = backupManager.backup() _backupFileUri.emit(backupFileUri) // Emit to trigger file share intent @@ -78,14 +78,14 @@ internal class SettingsViewModel @Inject constructor( _showErrorToast.emit(true) Log.e(TAG, "$e") } - _operation.update { false } + _backupInProgress.update { false } } } fun restore(uri: Uri) { backupJob?.cancel() backupJob = viewModelScope.launch(Dispatchers.IO) { - _operation.update { true } + _backupInProgress.update { true } try { backupManager.restore(uri) _showErrorToast.emit(false) @@ -93,7 +93,7 @@ internal class SettingsViewModel @Inject constructor( _showErrorToast.emit(true) Log.e(TAG, "$e") } - _operation.update { false } + _backupInProgress.update { false } } }