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,
baseModule,
calculatorModule,
backupModule,
badgesModule,
calendarModule,
contactsModule,
@ -87,6 +86,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
wikipediaModule,
servicesTagsModule,
widgetsServiceModule,
backupModule,
)
)
}

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.common
import android.net.Uri
import android.text.format.DateUtils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -13,14 +12,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.backup.BackupCompatibility
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage
@ -38,7 +35,6 @@ fun RestoreBackupSheet(
}
val state by viewModel.state
val selectedComponents by viewModel.selectedComponents
val compatibility by viewModel.compatibility
BottomSheetDialog(
@ -52,7 +48,6 @@ fun RestoreBackupSheet(
if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
Button(
enabled = selectedComponents.isNotEmpty(),
onClick = { viewModel.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.viewModelScope
import de.mm20.launcher2.backup.BackupCompatibility
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.backup.BackupManager
import de.mm20.launcher2.backup.BackupMetadata
import kotlinx.coroutines.launch
@ -21,9 +20,6 @@ class RestoreBackupSheetVM : ViewModel(), KoinComponent {
val state = mutableStateOf(RestoreBackupState.Parsing)
val metadata = mutableStateOf<BackupMetadata?>(null)
val compatibility = mutableStateOf<BackupCompatibility?>(null)
val selectedComponents = mutableStateOf(setOf<BackupComponent>())
val availableComponents = mutableStateOf(emptyList<BackupComponent>())
fun setInputUri(uri: Uri) {
restoreUri = uri
@ -32,33 +28,20 @@ class RestoreBackupSheetVM : ViewModel(), KoinComponent {
val metadata = backupManager.readBackupMeta(uri)
if (metadata == null) {
state.value = RestoreBackupState.InvalidFile
availableComponents.value = emptyList()
} else {
state.value = RestoreBackupState.Ready
compatibility.value = backupManager.checkCompatibility(metadata)
availableComponents.value = metadata.components.toList().sortedBy { it.ordinal }
}
selectedComponents.value = metadata?.components ?: emptySet()
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() {
val components = selectedComponents.value ?: return
val uri = restoreUri ?: return
viewModelScope.launch {
state.value = RestoreBackupState.Restoring
backupManager.restore(uri, components)
backupManager.restore(uri)
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.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage
@ -33,20 +32,23 @@ fun CreateBackupSheet(
val viewModel: CreateBackupSheetVM = viewModel()
LaunchedEffect(null) {
viewModel.reset()
}
val components by viewModel.selectedComponents
val state by viewModel.state
val backupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/vnd.de.mm20.launcher2.backup"),
onResult = {
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(
onDismissRequest = onDismissRequest,
@ -58,7 +60,6 @@ fun CreateBackupSheet(
confirmButton = {
if (state == CreateBackupState.Ready) {
Button(
enabled = components.isNotEmpty(),
onClick = {
val fileName = "${
ZonedDateTime.now().format(
@ -87,68 +88,7 @@ fun CreateBackupSheet(
) {
when (state) {
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 -> {
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.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.backup.BackupComponent
import de.mm20.launcher2.backup.BackupManager
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -16,30 +15,17 @@ class CreateBackupSheetVM : ViewModel(), KoinComponent {
val state = mutableStateOf(CreateBackupState.Ready)
val selectedComponents = mutableStateOf(BackupComponent.values().toSet())
fun reset() {
state.value = CreateBackupState.Ready
}
fun createBackup(uri: Uri) {
val components = selectedComponents.value ?: return
viewModelScope.launch {
state.value = CreateBackupState.BackingUp
backupManager.backup(uri, components)
backupManager.backup(uri)
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 {

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>
</plurals>
<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_component_favorites">Favorites &amp; hidden apps</string>
<string name="backup_component_settings">Settings</string>

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.preferences
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -11,6 +12,19 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.firstOrNull
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) {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val backupDataStore = DataStoreFactory.create(

View File

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

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity
@ -18,7 +19,7 @@ import org.json.JSONArray
import org.json.JSONException
import java.io.File
interface CustomAttributesRepository {
interface CustomAttributesRepository: Backupable {
fun search(query: String): Flow<ImmutableList<SavableSearchable>>
@ -32,9 +33,6 @@ interface CustomAttributesRepository {
fun setTags(searchable: SavableSearchable, tags: 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 getItemsForTag(tag: String): Flow<List<SavableSearchable>>
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()
var page = 0
do {
@ -212,7 +210,7 @@ internal class CustomAttributesRepositoryImpl(
} 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()
dao.wipeCustomAttributes()

View File

@ -1,7 +1,10 @@
package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.backup.Backupable
import org.koin.core.qualifier.named
import org.koin.dsl.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
import de.mm20.launcher2.backup.Backupable
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.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()) }
}

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.searchactions
import android.content.Context
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SearchActionEntity
@ -27,25 +28,23 @@ import org.json.JSONException
import java.io.File
import java.util.UUID
interface SearchActionRepository {
interface SearchActionRepository : Backupable {
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder>
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
}
internal class SearchActionRepositoryImpl(
private val context: Context,
private val database: AppDatabase
): SearchActionRepository {
) : SearchActionRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
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> {
@ -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()
var page = 0
var iconCounter = 0
@ -116,11 +115,12 @@ internal class SearchActionRepositoryImpl(
} 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()
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) {
val searchActions = mutableListOf<SearchActionEntity>()

View File

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

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.searchable
import android.content.Context
import android.util.Log
import androidx.room.withTransaction
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SavedSearchableEntity
@ -30,7 +31,7 @@ import org.koin.core.error.NoBeanDefFoundException
import org.koin.core.qualifier.named
import java.io.File
interface SavableSearchableRepository {
interface SavableSearchableRepository: Backupable {
fun insert(
searchable: SavableSearchable,
@ -107,9 +108,6 @@ interface SavableSearchableRepository {
*/
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
* - entries that cannot be deserialized anymore
@ -392,7 +390,7 @@ internal class SavableSearchableRepositoryImpl(
.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()
var page = 0
do {
@ -420,7 +418,7 @@ internal class SavableSearchableRepositoryImpl(
} 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()
dao.wipeFavorites()

View File

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

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.themes
import android.content.Context
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.CoroutineScope
@ -16,13 +17,12 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import java.io.File
import java.lang.IllegalArgumentException
import java.util.UUID
class ThemeRepository(
private val context: Context,
private val database: AppDatabase,
) {
) : Backupable {
private val scope = CoroutineScope(Dispatchers.IO + Job())
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 themes = dao.getAll().first().map { Theme(it) }
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()
dao.deleteAll()

View File

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

View File

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

View File

@ -17,7 +17,6 @@ data class BackupMetadata(
* Backup schema version in format x.y.
*/
val format: String,
val components: Set<BackupComponent>,
) {
internal suspend fun writeToFile(file: File) {
@ -26,7 +25,7 @@ data class BackupMetadata(
"timestamp" to timestamp,
"format" to format,
"versionName" to appVersionName,
"components" to JSONArray(components.map { it.value })
"components" to JSONArray()
)
withContext(Dispatchers.IO) {
file.outputStream().bufferedWriter().use {
@ -46,14 +45,6 @@ data class BackupMetadata(
timestamp = json.optLong("timestamp"),
format = json.optString("format"),
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) {
return@withContext null

View File

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