Refactor backup/restore

Close #575
This commit is contained in:
MM20 2023-10-28 17:15:04 +02:00
parent c9d9b3a4e9
commit 47cf4e9483
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
24 changed files with 93 additions and 329 deletions

View File

@ -62,7 +62,6 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
appShortcutsModule, appShortcutsModule,
baseModule, baseModule,
calculatorModule, calculatorModule,
backupModule,
badgesModule, badgesModule,
calendarModule, calendarModule,
contactsModule, contactsModule,
@ -87,6 +86,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
wikipediaModule, wikipediaModule,
servicesTagsModule, servicesTagsModule,
widgetsServiceModule, widgetsServiceModule,
backupModule,
) )
) )
} }

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.common
import android.net.Uri import android.net.Uri
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -13,14 +12,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.backup.BackupCompatibility import de.mm20.launcher2.backup.BackupCompatibility
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage
@ -38,7 +35,6 @@ fun RestoreBackupSheet(
} }
val state by viewModel.state val state by viewModel.state
val selectedComponents by viewModel.selectedComponents
val compatibility by viewModel.compatibility val compatibility by viewModel.compatibility
BottomSheetDialog( BottomSheetDialog(
@ -52,7 +48,6 @@ fun RestoreBackupSheet(
if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) { if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
Button( Button(
enabled = selectedComponents.isNotEmpty(),
onClick = { viewModel.restore() }) { onClick = { viewModel.restore() }) {
Text(stringResource(R.string.preference_restore)) Text(stringResource(R.string.preference_restore))
} }
@ -138,60 +133,6 @@ fun RestoreBackupSheet(
) )
) )
} }
Text(
stringResource(R.string.restore_select_components),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
)
val components by viewModel.availableComponents
for (component in components) {
Row(
modifier = Modifier
.clickable {
viewModel.toggleComponent(
component
)
}
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = when (component) {
BackupComponent.Favorites -> Icons.Rounded.Star
BackupComponent.Settings -> Icons.Rounded.Settings
BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward
BackupComponent.Widgets -> Icons.Rounded.Widgets
BackupComponent.Customizations -> Icons.Rounded.Edit
BackupComponent.Themes -> Icons.Rounded.Palette
},
contentDescription = null
)
Text(
text = stringResource(
when (component) {
BackupComponent.Favorites -> R.string.backup_component_favorites
BackupComponent.Settings -> R.string.backup_component_settings
BackupComponent.SearchActions -> R.string.backup_component_searchactions
BackupComponent.Widgets -> R.string.backup_component_widgets
BackupComponent.Customizations -> R.string.backup_component_customizations
BackupComponent.Themes -> R.string.backup_component_themes
}
),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
)
Checkbox(
checked = selectedComponents.contains(
component
),
onCheckedChange = {
viewModel.toggleComponent(component)
}
)
}
}
} }
} }
} }

View File

@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.backup.BackupCompatibility import de.mm20.launcher2.backup.BackupCompatibility
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.backup.BackupManager import de.mm20.launcher2.backup.BackupManager
import de.mm20.launcher2.backup.BackupMetadata import de.mm20.launcher2.backup.BackupMetadata
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -21,9 +20,6 @@ class RestoreBackupSheetVM : ViewModel(), KoinComponent {
val state = mutableStateOf(RestoreBackupState.Parsing) val state = mutableStateOf(RestoreBackupState.Parsing)
val metadata = mutableStateOf<BackupMetadata?>(null) val metadata = mutableStateOf<BackupMetadata?>(null)
val compatibility = mutableStateOf<BackupCompatibility?>(null) val compatibility = mutableStateOf<BackupCompatibility?>(null)
val selectedComponents = mutableStateOf(setOf<BackupComponent>())
val availableComponents = mutableStateOf(emptyList<BackupComponent>())
fun setInputUri(uri: Uri) { fun setInputUri(uri: Uri) {
restoreUri = uri restoreUri = uri
@ -32,33 +28,20 @@ class RestoreBackupSheetVM : ViewModel(), KoinComponent {
val metadata = backupManager.readBackupMeta(uri) val metadata = backupManager.readBackupMeta(uri)
if (metadata == null) { if (metadata == null) {
state.value = RestoreBackupState.InvalidFile state.value = RestoreBackupState.InvalidFile
availableComponents.value = emptyList()
} else { } else {
state.value = RestoreBackupState.Ready state.value = RestoreBackupState.Ready
compatibility.value = backupManager.checkCompatibility(metadata) compatibility.value = backupManager.checkCompatibility(metadata)
availableComponents.value = metadata.components.toList().sortedBy { it.ordinal }
} }
selectedComponents.value = metadata?.components ?: emptySet()
this@RestoreBackupSheetVM.metadata.value = metadata this@RestoreBackupSheetVM.metadata.value = metadata
} }
} }
fun toggleComponent(component: BackupComponent) {
val components = selectedComponents.value ?: emptySet()
if (components.contains(component)) {
selectedComponents.value = components - component
} else {
selectedComponents.value = components + component
}
}
fun restore() { fun restore() {
val components = selectedComponents.value ?: return
val uri = restoreUri ?: return val uri = restoreUri ?: return
viewModelScope.launch { viewModelScope.launch {
state.value = RestoreBackupState.Restoring state.value = RestoreBackupState.Restoring
backupManager.restore(uri, components) backupManager.restore(uri)
state.value = RestoreBackupState.Restored state.value = RestoreBackupState.Restored
} }
} }

View File

@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage
@ -33,20 +32,23 @@ fun CreateBackupSheet(
val viewModel: CreateBackupSheetVM = viewModel() val viewModel: CreateBackupSheetVM = viewModel()
LaunchedEffect(null) {
viewModel.reset()
}
val components by viewModel.selectedComponents
val state by viewModel.state
val backupLauncher = rememberLauncherForActivityResult( val backupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/vnd.de.mm20.launcher2.backup"), contract = ActivityResultContracts.CreateDocument("application/vnd.de.mm20.launcher2.backup"),
onResult = { onResult = {
if (it != null) viewModel.createBackup(it) if (it != null) viewModel.createBackup(it)
} }
) )
LaunchedEffect(null) {
viewModel.reset()
val fileName = "${
ZonedDateTime.now().format(
DateTimeFormatter.ISO_INSTANT
).replace(":", "_")
}.kvaesitso"
backupLauncher.launch(fileName)
}
val state by viewModel.state
BottomSheetDialog( BottomSheetDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -58,7 +60,6 @@ fun CreateBackupSheet(
confirmButton = { confirmButton = {
if (state == CreateBackupState.Ready) { if (state == CreateBackupState.Ready) {
Button( Button(
enabled = components.isNotEmpty(),
onClick = { onClick = {
val fileName = "${ val fileName = "${
ZonedDateTime.now().format( ZonedDateTime.now().format(
@ -87,68 +88,7 @@ fun CreateBackupSheet(
) { ) {
when (state) { when (state) {
CreateBackupState.Ready -> { CreateBackupState.Ready -> {
Column {
Text(
stringResource(R.string.backup_select_components),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
)
BackupableComponent(
title = stringResource(R.string.backup_component_settings),
icon = Icons.Rounded.Settings,
checked = components.contains(BackupComponent.Settings),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.Settings)
}
)
BackupableComponent(
title = stringResource(R.string.backup_component_favorites),
icon = Icons.Rounded.Star,
checked = components.contains(BackupComponent.Favorites),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.Favorites)
}
)
BackupableComponent(
title = stringResource(R.string.backup_component_widgets),
icon = Icons.Rounded.Widgets,
checked = components.contains(BackupComponent.Widgets),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.Widgets)
}
)
BackupableComponent(
title = stringResource(R.string.backup_component_customizations),
icon = Icons.Rounded.Edit,
checked = components.contains(BackupComponent.Customizations),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.Customizations)
}
)
BackupableComponent(
title = stringResource(R.string.backup_component_searchactions),
icon = Icons.Rounded.TravelExplore,
checked = components.contains(BackupComponent.SearchActions),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.SearchActions)
}
)
BackupableComponent(
title = stringResource(R.string.backup_component_themes),
icon = Icons.Rounded.Palette,
checked = components.contains(BackupComponent.Themes),
onCheckedChange = {
viewModel.toggleComponent(BackupComponent.Themes)
}
)
SmallMessage(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
icon = Icons.Rounded.Warning,
text = stringResource(R.string.backup_not_included)
)
}
} }
CreateBackupState.BackingUp -> { CreateBackupState.BackingUp -> {
Box( Box(
@ -176,36 +116,3 @@ fun CreateBackupSheet(
} }
} }
@Composable
fun BackupableComponent(
title: String,
icon: ImageVector,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier
.clickable {
onCheckedChange(!checked)
}
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null
)
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}

View File

@ -4,7 +4,6 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.backup.BackupManager import de.mm20.launcher2.backup.BackupManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -16,30 +15,17 @@ class CreateBackupSheetVM : ViewModel(), KoinComponent {
val state = mutableStateOf(CreateBackupState.Ready) val state = mutableStateOf(CreateBackupState.Ready)
val selectedComponents = mutableStateOf(BackupComponent.values().toSet())
fun reset() { fun reset() {
state.value = CreateBackupState.Ready state.value = CreateBackupState.Ready
} }
fun createBackup(uri: Uri) { fun createBackup(uri: Uri) {
val components = selectedComponents.value ?: return
viewModelScope.launch { viewModelScope.launch {
state.value = CreateBackupState.BackingUp state.value = CreateBackupState.BackingUp
backupManager.backup(uri, components) backupManager.backup(uri)
state.value = CreateBackupState.BackedUp state.value = CreateBackupState.BackedUp
} }
} }
fun toggleComponent(component: BackupComponent) {
val components = selectedComponents.value ?: emptySet()
if (components.contains(component)) {
selectedComponents.value = components - component
} else {
selectedComponents.value = components + component
}
}
} }
enum class CreateBackupState { enum class CreateBackupState {

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.backup
import java.io.File
interface Backupable {
suspend fun backup(toDir: File)
suspend fun restore(fromDir: File)
}

View File

@ -701,6 +701,7 @@
<item quantity="other">Full in %1$s minutes</item> <item quantity="other">Full in %1$s minutes</item>
</plurals> </plurals>
<string name="backup_select_components">Select what to backup:</string> <string name="backup_select_components">Select what to backup:</string>
<string name="backup_info">The following data will be backed up:</string>
<string name="backup_not_included">Connected accounts and 3rd party app widgets will not be backed up.</string> <string name="backup_not_included">Connected accounts and 3rd party app widgets will not be backed up.</string>
<string name="backup_component_favorites">Favorites &amp; hidden apps</string> <string name="backup_component_favorites">Favorites &amp; hidden apps</string>
<string name="backup_component_settings">Settings</string> <string name="backup_component_settings">Settings</string>

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.preferences
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -11,6 +12,19 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import java.io.File import java.io.File
internal class LauncherStoreBackupComponent(
private val context: Context,
private val dataStore: LauncherDataStore
): Backupable {
override suspend fun backup(toDir: File) {
dataStore.export(toDir)
}
override suspend fun restore(fromDir: File) {
dataStore.import(context, fromDir)
}
}
suspend fun LauncherDataStore.export(toDir: File) { suspend fun LauncherDataStore.export(toDir: File) {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val backupDataStore = DataStoreFactory.create( val backupDataStore = DataStoreFactory.create(

View File

@ -1,8 +1,11 @@
package de.mm20.launcher2.preferences package de.mm20.launcher2.preferences
import de.mm20.launcher2.backup.Backupable
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val preferencesModule = module { val preferencesModule = module {
single { androidContext().dataStore } single { androidContext().dataStore }
factory<Backupable>(named<LauncherDataStore>()) { LauncherStoreBackupComponent(androidContext(), get()) }
} }

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.data.customattrs package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity import de.mm20.launcher2.database.entities.CustomAttributeEntity
@ -18,7 +19,7 @@ import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import java.io.File import java.io.File
interface CustomAttributesRepository { interface CustomAttributesRepository: Backupable {
fun search(query: String): Flow<ImmutableList<SavableSearchable>> fun search(query: String): Flow<ImmutableList<SavableSearchable>>
@ -32,9 +33,6 @@ interface CustomAttributesRepository {
fun setTags(searchable: SavableSearchable, tags: List<String>) fun setTags(searchable: SavableSearchable, tags: List<String>)
fun getTags(searchable: SavableSearchable): Flow<List<String>> fun getTags(searchable: SavableSearchable): Flow<List<String>>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
fun getAllTags(startsWith: String? = null): Flow<List<String>> fun getAllTags(startsWith: String? = null): Flow<List<String>>
fun getItemsForTag(tag: String): Flow<List<SavableSearchable>> fun getItemsForTag(tag: String): Flow<List<SavableSearchable>>
fun setItemsForTag(tag: String, items: List<SavableSearchable>): Job fun setItemsForTag(tag: String, items: List<SavableSearchable>): Job
@ -188,7 +186,7 @@ internal class CustomAttributesRepositoryImpl(
} }
} }
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao() val dao = appDatabase.backupDao()
var page = 0 var page = 0
do { do {
@ -212,7 +210,7 @@ internal class CustomAttributesRepositoryImpl(
} while (customAttrs.size == 100) } while (customAttrs.size == 100)
} }
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao() val dao = appDatabase.backupDao()
dao.wipeCustomAttributes() dao.wipeCustomAttributes()

View File

@ -1,7 +1,10 @@
package de.mm20.launcher2.data.customattrs package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.backup.Backupable
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val customAttrsModule = module { val customAttrsModule = module {
single<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get(), get()) } factory<Backupable>(named<CustomAttributesRepository>()) { CustomAttributesRepositoryImpl(get(), get()) }
factory<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get(), get()) }
} }

View File

@ -1,9 +1,12 @@
package de.mm20.launcher2.searchactions package de.mm20.launcher2.searchactions
import de.mm20.launcher2.backup.Backupable
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val searchActionsModule = module { val searchActionsModule = module {
single<SearchActionRepository> { SearchActionRepositoryImpl(androidContext(), get()) } factory<Backupable>(named<SearchActionRepository>()) { SearchActionRepositoryImpl(androidContext(), get()) }
factory<SearchActionRepository> { SearchActionRepositoryImpl(androidContext(), get()) }
single<SearchActionService> { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) } single<SearchActionService> { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) }
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.searchactions package de.mm20.launcher2.searchactions
import android.content.Context import android.content.Context
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SearchActionEntity import de.mm20.launcher2.database.entities.SearchActionEntity
@ -27,25 +28,23 @@ import org.json.JSONException
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
interface SearchActionRepository { interface SearchActionRepository : Backupable {
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder> fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder>
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>) fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
} }
internal class SearchActionRepositoryImpl( internal class SearchActionRepositoryImpl(
private val context: Context, private val context: Context,
private val database: AppDatabase private val database: AppDatabase
): SearchActionRepository { ) : SearchActionRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> { override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
val dao = database.searchActionDao() val dao = database.searchActionDao()
return dao.getSearchActions().map { it.mapNotNull { SearchActionBuilder.from(context, it) } } return dao.getSearchActions()
.map { it.mapNotNull { SearchActionBuilder.from(context, it) } }
} }
override fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder> { override fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder> {
@ -73,7 +72,7 @@ internal class SearchActionRepositoryImpl(
} }
} }
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
var page = 0 var page = 0
var iconCounter = 0 var iconCounter = 0
@ -116,11 +115,12 @@ internal class SearchActionRepositoryImpl(
} while (websearches.size == 100) } while (websearches.size == 100)
} }
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
dao.wipeSearchActions() dao.wipeSearchActions()
val files = fromDir.listFiles { _, name -> name.startsWith("searchactions.") } ?: return@withContext val files =
fromDir.listFiles { _, name -> name.startsWith("searchactions.") } ?: return@withContext
for (file in files) { for (file in files) {
val searchActions = mutableListOf<SearchActionEntity>() val searchActions = mutableListOf<SearchActionEntity>()

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.searchable package de.mm20.launcher2.searchable
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@ -7,6 +8,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val searchableModule = module { val searchableModule = module {
factory <Backupable>(named<SavableSearchableRepository>()) { SavableSearchableRepositoryImpl(androidContext(), get(), get()) }
factory <SavableSearchableRepository> { SavableSearchableRepositoryImpl(androidContext(), get(), get()) } factory <SavableSearchableRepository> { SavableSearchableRepositoryImpl(androidContext(), get(), get()) }
factory<SearchableDeserializer>(named(Tag.Domain)) { TagDeserializer() } factory<SearchableDeserializer>(named(Tag.Domain)) { TagDeserializer() }
} }

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.searchable
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.room.withTransaction import androidx.room.withTransaction
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SavedSearchableEntity import de.mm20.launcher2.database.entities.SavedSearchableEntity
@ -30,7 +31,7 @@ import org.koin.core.error.NoBeanDefFoundException
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import java.io.File import java.io.File
interface SavableSearchableRepository { interface SavableSearchableRepository: Backupable {
fun insert( fun insert(
searchable: SavableSearchable, searchable: SavableSearchable,
@ -107,9 +108,6 @@ interface SavableSearchableRepository {
*/ */
suspend fun getByKeys(keys: List<String>): List<SavableSearchable> suspend fun getByKeys(keys: List<String>): List<SavableSearchable>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
/** /**
* Remove database entries that are invalid. This includes * Remove database entries that are invalid. This includes
* - entries that cannot be deserialized anymore * - entries that cannot be deserialized anymore
@ -392,7 +390,7 @@ internal class SavableSearchableRepositoryImpl(
.mapNotNull { fromDatabaseEntity(it).searchable } .mapNotNull { fromDatabaseEntity(it).searchable }
} }
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
var page = 0 var page = 0
do { do {
@ -420,7 +418,7 @@ internal class SavableSearchableRepositoryImpl(
} while (favorites.size == 100) } while (favorites.size == 100)
} }
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
dao.wipeFavorites() dao.wipeFavorites()

View File

@ -1,7 +1,10 @@
package de.mm20.launcher2.themes package de.mm20.launcher2.themes
import de.mm20.launcher2.backup.Backupable
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val themesModule = module { val themesModule = module {
factory<Backupable>(named<ThemeRepository>()) { ThemeRepository(get(), get()) }
factory { ThemeRepository(get(), get()) } factory { ThemeRepository(get(), get()) }
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.themes package de.mm20.launcher2.themes
import android.content.Context import android.content.Context
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -16,13 +17,12 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import java.io.File import java.io.File
import java.lang.IllegalArgumentException
import java.util.UUID import java.util.UUID
class ThemeRepository( class ThemeRepository(
private val context: Context, private val context: Context,
private val database: AppDatabase, private val database: AppDatabase,
) { ) : Backupable {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
fun getThemes(): Flow<List<Theme>> { fun getThemes(): Flow<List<Theme>> {
@ -89,7 +89,7 @@ class ThemeRepository(
} }
} }
suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao() val dao = database.themeDao()
val themes = dao.getAll().first().map { Theme(it) } val themes = dao.getAll().first().map { Theme(it) }
val data = ThemeJson.encodeToString(themes) val data = ThemeJson.encodeToString(themes)
@ -100,7 +100,7 @@ class ThemeRepository(
} }
} }
suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao() val dao = database.themeDao()
dao.deleteAll() dao.deleteAll()

View File

@ -1,7 +1,10 @@
package de.mm20.launcher2.widgets package de.mm20.launcher2.widgets
import de.mm20.launcher2.backup.Backupable
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val widgetsModule = module { val widgetsModule = module {
single<WidgetRepository> { WidgetRepositoryImpl(get()) } factory<Backupable>(named<WidgetRepository>()) { WidgetRepositoryImpl(get()) }
factory<WidgetRepository> { WidgetRepositoryImpl(get()) }
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.widgets package de.mm20.launcher2.widgets
import androidx.room.withTransaction import androidx.room.withTransaction
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.WidgetEntity import de.mm20.launcher2.database.entities.WidgetEntity
@ -13,7 +14,7 @@ import org.json.JSONException
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
interface WidgetRepository { interface WidgetRepository: Backupable {
fun get(parent: UUID? = null, limit: Int = 100, offset: Int = 0): Flow<List<Widget>> fun get(parent: UUID? = null, limit: Int = 100, offset: Int = 0): Flow<List<Widget>>
fun update(widget: Widget) fun update(widget: Widget)
fun create(widget: Widget, position: Int, parentId: UUID? = null) fun create(widget: Widget, position: Int, parentId: UUID? = null)
@ -22,9 +23,6 @@ interface WidgetRepository {
fun exists(type: String): Flow<Boolean> fun exists(type: String): Flow<Boolean>
fun count(type: String): Flow<Int> fun count(type: String): Flow<Int>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
} }
internal class WidgetRepositoryImpl( internal class WidgetRepositoryImpl(
@ -93,7 +91,7 @@ internal class WidgetRepositoryImpl(
} }
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
var page = 0 var page = 0
do { do {
@ -119,7 +117,7 @@ internal class WidgetRepositoryImpl(
} while (widgets.size == 100) } while (widgets.size == 100)
} }
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
dao.wipeWidgets() dao.wipeWidgets()

View File

@ -39,12 +39,6 @@ dependencies {
implementation(libs.koin.android) implementation(libs.koin.android)
implementation(project(":data:searchable")) implementation(project(":core:base"))
implementation(project(":data:widgets"))
implementation(project(":data:search-actions"))
implementation(project(":core:preferences"))
implementation(project(":core:ktx")) implementation(project(":core:ktx"))
implementation(project(":data:customattrs"))
implementation(project(":data:themes"))
} }

View File

@ -1,16 +0,0 @@
package de.mm20.launcher2.backup
enum class BackupComponent(val value: String) {
Settings("settings"),
Favorites("favorites"),
Widgets("widgets2"),
Customizations("customizations"),
SearchActions("searchactions"),
Themes("themes");
companion object {
fun fromValue(value: String): BackupComponent? {
return entries.firstOrNull { it.value == value }
}
}
}

View File

@ -3,14 +3,6 @@ package de.mm20.launcher2.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.export
import de.mm20.launcher2.preferences.import
import de.mm20.launcher2.searchactions.SearchActionRepository
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -21,12 +13,7 @@ import java.util.zip.ZipOutputStream
class BackupManager( class BackupManager(
private val context: Context, private val context: Context,
private val dataStore: LauncherDataStore, private val components: List<Backupable>,
private val searchableRepository: SavableSearchableRepository,
private val widgetRepository: WidgetRepository,
private val searchActionRepository: SearchActionRepository,
private val customAttrsRepository: CustomAttributesRepository,
private val themesRepository: ThemeRepository,
) { ) {
private val scope = CoroutineScope(Dispatchers.Default + Job()) private val scope = CoroutineScope(Dispatchers.Default + Job())
@ -35,8 +22,7 @@ class BackupManager(
* @return Uri to the created backup archive * @return Uri to the created backup archive
*/ */
suspend fun backup( suspend fun backup(
uri: Uri, uri: Uri
include: Set<BackupComponent> = BackupComponent.entries.toSet()
) { ) {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
@ -45,7 +31,6 @@ class BackupManager(
appVersionName = packageInfo.versionName, appVersionName = packageInfo.versionName,
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
deviceName = Build.MODEL, deviceName = Build.MODEL,
components = include,
format = BackupFormat, format = BackupFormat,
) )
@ -60,28 +45,8 @@ class BackupManager(
val metaFile = File(backupDir, "meta") val metaFile = File(backupDir, "meta")
meta.writeToFile(metaFile) meta.writeToFile(metaFile)
if (include.contains(BackupComponent.Settings)) { for (component in components) {
dataStore.export(backupDir) component.backup(backupDir)
}
if (include.contains(BackupComponent.Favorites)) {
searchableRepository.export(backupDir)
}
if (include.contains(BackupComponent.Widgets)) {
widgetRepository.export(backupDir)
}
if (include.contains(BackupComponent.SearchActions)) {
searchActionRepository.export(backupDir)
}
if (include.contains(BackupComponent.Customizations)) {
customAttrsRepository.export(backupDir)
}
if (include.contains(BackupComponent.Themes)) {
themesRepository.export(backupDir)
} }
createArchive(backupDir, outputStream) createArchive(backupDir, outputStream)
@ -92,7 +57,6 @@ class BackupManager(
suspend fun restore( suspend fun restore(
uri: Uri, uri: Uri,
include: Set<BackupComponent> = BackupComponent.values().toSet()
) { ) {
val job = scope.launch { val job = scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -105,28 +69,8 @@ class BackupManager(
extractArchive(inputStream, restoreDir) extractArchive(inputStream, restoreDir)
inputStream.close() inputStream.close()
if (include.contains(BackupComponent.Settings)) { for (component in components) {
dataStore.import(context, restoreDir) component.restore(restoreDir)
}
if (include.contains(BackupComponent.Favorites)) {
searchableRepository.import(restoreDir)
}
if (include.contains(BackupComponent.Widgets)) {
widgetRepository.import(restoreDir)
}
if (include.contains(BackupComponent.SearchActions)) {
searchActionRepository.import(restoreDir)
}
if (include.contains(BackupComponent.Customizations)) {
customAttrsRepository.import(restoreDir)
}
if (include.contains(BackupComponent.Themes)) {
themesRepository.import(restoreDir)
} }
} }
} }
@ -198,7 +142,7 @@ class BackupManager(
*/ */
private const val BackupFormatMajor = 1 private const val BackupFormatMajor = 1
private const val BackupFormatMinor = 7 private const val BackupFormatMinor = 8
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
} }
} }

View File

@ -17,7 +17,6 @@ data class BackupMetadata(
* Backup schema version in format x.y. * Backup schema version in format x.y.
*/ */
val format: String, val format: String,
val components: Set<BackupComponent>,
) { ) {
internal suspend fun writeToFile(file: File) { internal suspend fun writeToFile(file: File) {
@ -26,7 +25,7 @@ data class BackupMetadata(
"timestamp" to timestamp, "timestamp" to timestamp,
"format" to format, "format" to format,
"versionName" to appVersionName, "versionName" to appVersionName,
"components" to JSONArray(components.map { it.value }) "components" to JSONArray()
) )
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
file.outputStream().bufferedWriter().use { file.outputStream().bufferedWriter().use {
@ -46,14 +45,6 @@ data class BackupMetadata(
timestamp = json.optLong("timestamp"), timestamp = json.optLong("timestamp"),
format = json.optString("format"), format = json.optString("format"),
appVersionName = json.optString("versionName"), appVersionName = json.optString("versionName"),
components = json.getJSONArray("components").let {
val set = mutableSetOf<BackupComponent>()
for (i in 0 until it.length()) {
val component = BackupComponent.fromValue(it.getString(i))
if (component != null) set.add(component)
}
set
}
) )
} catch (e: JSONException) { } catch (e: JSONException) {
return@withContext null return@withContext null

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val backupModule = module { val backupModule = module {
single { BackupManager(androidContext(), get(), get(), get(), get(), get(), get()) } single { BackupManager(androidContext(), getAll()) }
} }