Backup & Restore for custom attributes
This commit is contained in:
parent
2e3add0d94
commit
f069d5f6f4
@ -45,5 +45,6 @@ dependencies {
|
|||||||
implementation(project(":widgets"))
|
implementation(project(":widgets"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
implementation(project(":customattrs"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ enum class BackupComponent(val value: String) {
|
|||||||
Settings("settings"),
|
Settings("settings"),
|
||||||
Favorites("favorites"),
|
Favorites("favorites"),
|
||||||
Widgets("widgets"),
|
Widgets("widgets"),
|
||||||
|
Customizations("customizations"),
|
||||||
Websearches("websearches");
|
Websearches("websearches");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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.customattrs.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.preferences.export
|
import de.mm20.launcher2.preferences.export
|
||||||
@ -25,6 +26,7 @@ class BackupManager(
|
|||||||
private val favoritesRepository: FavoritesRepository,
|
private val favoritesRepository: FavoritesRepository,
|
||||||
private val widgetRepository: WidgetRepository,
|
private val widgetRepository: WidgetRepository,
|
||||||
private val websearchRepository: WebsearchRepository,
|
private val websearchRepository: WebsearchRepository,
|
||||||
|
private val customAttrsRepository: CustomAttributesRepository,
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||||
|
|
||||||
@ -74,6 +76,10 @@ class BackupManager(
|
|||||||
websearchRepository.export(backupDir)
|
websearchRepository.export(backupDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (include.contains(BackupComponent.Customizations)) {
|
||||||
|
customAttrsRepository.export(backupDir)
|
||||||
|
}
|
||||||
|
|
||||||
createArchive(backupDir, outputStream)
|
createArchive(backupDir, outputStream)
|
||||||
outputStream.close()
|
outputStream.close()
|
||||||
|
|
||||||
@ -110,6 +116,10 @@ class BackupManager(
|
|||||||
if (include.contains(BackupComponent.Websearches)) {
|
if (include.contains(BackupComponent.Websearches)) {
|
||||||
websearchRepository.import(restoreDir)
|
websearchRepository.import(restoreDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (include.contains(BackupComponent.Customizations)) {
|
||||||
|
customAttrsRepository.import(restoreDir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
job.join()
|
job.join()
|
||||||
@ -175,7 +185,7 @@ class BackupManager(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BackupFormatMajor = 1
|
private const val BackupFormatMajor = 1
|
||||||
private const val BackupFormatMinor = 0
|
private const val BackupFormatMinor = 1
|
||||||
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
|
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()) }
|
single { BackupManager(androidContext(), get(), get(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
@ -43,5 +43,6 @@ dependencies {
|
|||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
implementation(project(":crashreporter"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,17 +1,24 @@
|
|||||||
package de.mm20.launcher2.customattrs
|
package de.mm20.launcher2.customattrs
|
||||||
|
|
||||||
|
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.WebsearchEntity
|
||||||
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
interface CustomAttributesRepository {
|
interface CustomAttributesRepository {
|
||||||
fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?>
|
fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?>
|
||||||
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?)
|
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?)
|
||||||
|
|
||||||
|
suspend fun export(toDir: File)
|
||||||
|
suspend fun import(fromDir: File)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CustomAttributesRepositoryImpl(
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
import de.mm20.launcher2.database.entities.WebsearchEntity
|
import de.mm20.launcher2.database.entities.WebsearchEntity
|
||||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||||
@ -37,4 +38,13 @@ interface BackupRestoreDao {
|
|||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun importWebsearches(items: List<WebsearchEntity>)
|
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>)
|
||||||
}
|
}
|
||||||
@ -610,6 +610,7 @@
|
|||||||
<string name="backup_component_settings">Settings</string>
|
<string name="backup_component_settings">Settings</string>
|
||||||
<string name="backup_component_websearches">Web search shortcuts</string>
|
<string name="backup_component_websearches">Web search shortcuts</string>
|
||||||
<string name="backup_component_widgets">Built-in widgets</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="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>
|
<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 -->
|
<!-- %1$s: app name -->
|
||||||
|
|||||||
@ -173,6 +173,7 @@ fun RestoreBackupSheet(
|
|||||||
BackupComponent.Settings -> Icons.Rounded.Settings
|
BackupComponent.Settings -> Icons.Rounded.Settings
|
||||||
BackupComponent.Websearches -> Icons.Rounded.TravelExplore
|
BackupComponent.Websearches -> Icons.Rounded.TravelExplore
|
||||||
BackupComponent.Widgets -> Icons.Rounded.Widgets
|
BackupComponent.Widgets -> Icons.Rounded.Widgets
|
||||||
|
BackupComponent.Customizations -> Icons.Rounded.Edit
|
||||||
},
|
},
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
@ -183,6 +184,7 @@ fun RestoreBackupSheet(
|
|||||||
BackupComponent.Settings -> R.string.backup_component_settings
|
BackupComponent.Settings -> R.string.backup_component_settings
|
||||||
BackupComponent.Websearches -> R.string.backup_component_websearches
|
BackupComponent.Websearches -> R.string.backup_component_websearches
|
||||||
BackupComponent.Widgets -> R.string.backup_component_widgets
|
BackupComponent.Widgets -> R.string.backup_component_widgets
|
||||||
|
BackupComponent.Customizations -> R.string.backup_component_customizations
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
|||||||
@ -126,6 +126,14 @@ fun CreateBackupSheet(
|
|||||||
viewModel.toggleComponent(BackupComponent.Widgets)
|
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(
|
BackupableComponent(
|
||||||
title = stringResource(R.string.backup_component_websearches),
|
title = stringResource(R.string.backup_component_websearches),
|
||||||
icon = Icons.Rounded.TravelExplore,
|
icon = Icons.Rounded.TravelExplore,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user