parent
c9d9b3a4e9
commit
47cf4e9483
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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 & hidden apps</string>
|
<string name="backup_component_favorites">Favorites & hidden apps</string>
|
||||||
<string name="backup_component_settings">Settings</string>
|
<string name="backup_component_settings">Settings</string>
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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>()
|
||||||
|
|||||||
@ -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() }
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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"))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user