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