Backup & Restore for custom attributes

This commit is contained in:
MM20 2022-07-27 21:08:07 +02:00
parent 2e3add0d94
commit f069d5f6f4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
10 changed files with 102 additions and 6 deletions

View File

@ -45,5 +45,6 @@ dependencies {
implementation(project(":widgets"))
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":customattrs"))
}

View File

@ -4,6 +4,7 @@ enum class BackupComponent(val value: String) {
Settings("settings"),
Favorites("favorites"),
Widgets("widgets"),
Customizations("customizations"),
Websearches("websearches");
companion object {

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.backup
import android.content.Context
import android.net.Uri
import android.os.Build
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.export
@ -25,6 +26,7 @@ class BackupManager(
private val favoritesRepository: FavoritesRepository,
private val widgetRepository: WidgetRepository,
private val websearchRepository: WebsearchRepository,
private val customAttrsRepository: CustomAttributesRepository,
) {
private val scope = CoroutineScope(Dispatchers.Default + Job())
@ -74,6 +76,10 @@ class BackupManager(
websearchRepository.export(backupDir)
}
if (include.contains(BackupComponent.Customizations)) {
customAttrsRepository.export(backupDir)
}
createArchive(backupDir, outputStream)
outputStream.close()
@ -110,6 +116,10 @@ class BackupManager(
if (include.contains(BackupComponent.Websearches)) {
websearchRepository.import(restoreDir)
}
if (include.contains(BackupComponent.Customizations)) {
customAttrsRepository.import(restoreDir)
}
}
}
job.join()
@ -175,7 +185,7 @@ class BackupManager(
companion object {
private const val BackupFormatMajor = 1
private const val BackupFormatMinor = 0
private const val BackupFormatMinor = 1
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
}
}

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()) }
single { BackupManager(androidContext(), get(), get(), get(), get(), get()) }
}

View File

@ -43,5 +43,6 @@ dependencies {
implementation(project(":database"))
implementation(project(":search"))
implementation(project(":ktx"))
implementation(project(":crashreporter"))
}

View File

@ -1,17 +1,24 @@
package de.mm20.launcher2.customattrs
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.database.entities.WebsearchEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import java.io.File
interface CustomAttributesRepository {
fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?>
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?)
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
}
internal class CustomAttributesRepositoryImpl(
@ -36,4 +43,59 @@ internal class CustomAttributesRepositoryImpl(
}
}
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao()
var page = 0
do {
val customAttrs = dao.exportCustomAttributes(limit = 100, offset = page * 100)
val jsonArray = JSONArray()
for (customAttr in customAttrs) {
jsonArray.put(
jsonObjectOf(
"key" to customAttr.key,
"value" to customAttr.value,
"type" to customAttr.type,
)
)
}
val file = File(toDir, "customizations.${page.toString().padStart(4, '0')}")
file.bufferedWriter().use {
it.write(jsonArray.toString())
}
page++
} while (customAttrs.size == 100)
}
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao()
dao.wipeCustomAttributes()
val files = fromDir.listFiles { _, name -> name.startsWith("customizations.") } ?: return@withContext
for (file in files) {
val customAttrs = mutableListOf<CustomAttributeEntity>()
try {
val jsonArray = JSONArray(file.inputStream().reader().readText())
for (i in 0 until jsonArray.length()) {
val json = jsonArray.getJSONObject(i)
val entity = CustomAttributeEntity(
id = null,
type = json.getString("type"),
value = json.optString("value"),
key = json.optString("key"),
)
customAttrs.add(entity)
}
dao.importCustomAttributes(customAttrs)
} catch (e: JSONException) {
CrashReporter.logException(e)
}
}
}
}

View File

@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.database.entities.WebsearchEntity
import de.mm20.launcher2.database.entities.WidgetEntity
@ -37,4 +38,13 @@ interface BackupRestoreDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun importWebsearches(items: List<WebsearchEntity>)
@Query("DELETE FROM CustomAttributes")
suspend fun wipeCustomAttributes()
@Query("SELECT * FROM CustomAttributes LIMIT :limit OFFSET :offset")
suspend fun exportCustomAttributes(limit: Int, offset: Int): List<CustomAttributeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun importCustomAttributes(items: List<CustomAttributeEntity>)
}

View File

@ -610,6 +610,7 @@
<string name="backup_component_settings">Settings</string>
<string name="backup_component_websearches">Web search shortcuts</string>
<string name="backup_component_widgets">Built-in widgets</string>
<string name="backup_component_customizations">Customizations</string>
<string name="backup_complete">The backup has been completed.</string>
<string name="restore_invalid_file">The selected file does not appear to be a backup. Are you sure you selected the right file?</string>
<!-- %1$s: app name -->

View File

@ -173,6 +173,7 @@ fun RestoreBackupSheet(
BackupComponent.Settings -> Icons.Rounded.Settings
BackupComponent.Websearches -> Icons.Rounded.TravelExplore
BackupComponent.Widgets -> Icons.Rounded.Widgets
BackupComponent.Customizations -> Icons.Rounded.Edit
},
contentDescription = null
)
@ -183,6 +184,7 @@ fun RestoreBackupSheet(
BackupComponent.Settings -> R.string.backup_component_settings
BackupComponent.Websearches -> R.string.backup_component_websearches
BackupComponent.Widgets -> R.string.backup_component_widgets
BackupComponent.Customizations -> R.string.backup_component_customizations
}
),
style = MaterialTheme.typography.titleMedium,

View File

@ -126,6 +126,14 @@ fun CreateBackupSheet(
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_websearches),
icon = Icons.Rounded.TravelExplore,