diff --git a/backup/build.gradle.kts b/backup/build.gradle.kts index ec9437f0..a6f5b770 100644 --- a/backup/build.gradle.kts +++ b/backup/build.gradle.kts @@ -45,5 +45,6 @@ dependencies { implementation(project(":widgets")) implementation(project(":preferences")) implementation(project(":ktx")) + implementation(project(":customattrs")) } \ No newline at end of file diff --git a/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt b/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt index 75ebda90..b06c77d1 100644 --- a/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt +++ b/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt @@ -4,6 +4,7 @@ enum class BackupComponent(val value: String) { Settings("settings"), Favorites("favorites"), Widgets("widgets"), + Customizations("customizations"), Websearches("websearches"); companion object { diff --git a/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt b/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt index bf6f21a9..d4d53c76 100644 --- a/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt +++ b/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt @@ -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" } } diff --git a/backup/src/main/java/de/mm20/launcher2/backup/Module.kt b/backup/src/main/java/de/mm20/launcher2/backup/Module.kt index a90dac10..1a94ceda 100644 --- a/backup/src/main/java/de/mm20/launcher2/backup/Module.kt +++ b/backup/src/main/java/de/mm20/launcher2/backup/Module.kt @@ -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()) } } \ No newline at end of file diff --git a/customattrs/build.gradle.kts b/customattrs/build.gradle.kts index cb82cac1..e4d18144 100644 --- a/customattrs/build.gradle.kts +++ b/customattrs/build.gradle.kts @@ -43,5 +43,6 @@ dependencies { implementation(project(":database")) implementation(project(":search")) implementation(project(":ktx")) + implementation(project(":crashreporter")) } \ No newline at end of file diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt index b4b3491a..a9900be9 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt @@ -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 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() + 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) + } + } + } } \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt b/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt index a4fa6481..780255b3 100644 --- a/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt @@ -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) + + @Query("DELETE FROM CustomAttributes") + suspend fun wipeCustomAttributes() + + @Query("SELECT * FROM CustomAttributes LIMIT :limit OFFSET :offset") + suspend fun exportCustomAttributes(limit: Int, offset: Int): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun importCustomAttributes(items: List) } \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 7401ad9e..a6d8c88c 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -610,6 +610,7 @@ Settings Web search shortcuts Built-in widgets + Customizations The backup has been completed. The selected file does not appear to be a backup. Are you sure you selected the right file? diff --git a/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt index 3cf4c135..6955c103 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt @@ -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, diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt index c8544128..fecd12e9 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt @@ -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,