From 4c01ab2e6661cab866d0dcd70a10a04ce11dd6a2 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sat, 3 May 2025 02:26:33 -0500 Subject: [PATCH] Import backup file in two passes Import non-task data before loading tasks --- .../org/tasks/backup/TasksJsonImporter.kt | 337 ++++++++++-------- 1 file changed, 196 insertions(+), 141 deletions(-) diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt index abeabe6cc..3a6fe457d 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt @@ -15,6 +15,8 @@ import com.todoroo.astrid.service.Upgrader.Companion.V12_4 import com.todoroo.astrid.service.Upgrader.Companion.V12_8 import com.todoroo.astrid.service.Upgrader.Companion.V6_4 import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.tasks.LocalBroadcastManager @@ -85,9 +87,37 @@ class TasksJsonImporter @Inject constructor( handler.post { progressDialog.setMessage(message) } } - suspend fun importTasks(context: Context, backupFile: Uri?, progressDialog: ProgressDialog?): ImportResult { + suspend fun importTasks( + context: Context, + backupFile: Uri?, + progressDialog: ProgressDialog? + ): ImportResult = withContext(Dispatchers.IO) { Timber.d("Importing backup file $backupFile") - val handler = Handler(context.mainLooper) + try { + val version = importMetadata(context, backupFile) + importTasks(context, backupFile, progressDialog, version) + if (version < Upgrader.V8_2) { + val themeIndex = preferences.getInt(R.string.p_theme_color, 7) + preferences.setInt( + R.string.p_theme_color, + getAndroidColor(context, themeIndex)) + } + if (version < Upgrader.V9_6) { + taskMover.migrateLocalTasks() + } + Timber.d("Updating parents") + caldavDao.updateParents() + } catch (e: IOException) { + Timber.e(e) + } + localBroadcastManager.broadcastRefresh() + result + } + + private suspend fun importMetadata( + context: Context, + backupFile: Uri?, + ): Int { val `is`: InputStream? = try { context.contentResolver.openInputStream(backupFile!!) } catch (e: FileNotFoundException) { @@ -97,167 +127,192 @@ class TasksJsonImporter @Inject constructor( val reader = JsonReader(bufferedReader) reader.isLenient = true val ignoreKeys = ignorePrefs.map { context.getString(it) } - try { - reader.beginObject() - var version = 0 - while (reader.hasNext()) { - when (val name = reader.nextName()) { - "version" -> version = reader.nextInt().also { Timber.d("Backup version: $it") } - "timestamp" -> reader.nextLong().let { Timber.d("Backup timestamp: $it") } - "data" -> { - reader.beginObject() - while (reader.hasNext()) { - when (val element = reader.nextName()) { - "tasks" -> { - reader.forEach { backup -> - result.taskCount++ - setProgressMessage( - handler, - progressDialog, - context.getString(R.string.import_progress_read, result.taskCount)) - importTask(backup, version) - } + reader.beginObject() + var version = 0 + while (reader.hasNext()) { + when (val name = reader.nextName()) { + "version" -> version = reader.nextInt().also { Timber.d("Backup version: $it") } + "timestamp" -> reader.nextLong().let { Timber.d("Backup timestamp: $it") } + "data" -> { + reader.beginObject() + while (reader.hasNext()) { + when (val element = reader.nextName()) { + "places" -> reader.forEach { place -> + if (locationDao.getByUid(place.uid!!) == null) { + locationDao.insert( + place.copy(icon = place.icon.migrateLegacyIcon()) + ) } - "places" -> reader.forEach { place -> - if (locationDao.getByUid(place.uid!!) == null) { - locationDao.insert( - place.copy(icon = place.icon.migrateLegacyIcon()) - ) - } + } + "tags" -> reader.forEach { tagData -> + findTagData(tagData)?.let { + return@forEach } - "tags" -> reader.forEach { tagData -> - findTagData(tagData)?.let { - return@forEach - } - tagDataDao.insert( - tagData.copy( - color = themeToColor(context, version, tagData.color ?: 0), - icon = tagData.icon.migrateLegacyIcon(), - ) + tagDataDao.insert( + tagData.copy( + color = themeToColor(context, version, tagData.color ?: 0), + icon = tagData.icon.migrateLegacyIcon(), ) - } - "filters" -> reader.forEach { - it - .let { - if (version < Upgrade_13_2.VERSION) - filterCriteriaProvider.rebuildFilter(it) - else - it - } - .let { filter -> - if (filterDao.getByName(filter.title!!) == null) { - filterDao.insert( - filter.copy( - color = themeToColor(context, version, filter.color ?: 0), - icon = filter.icon.migrateLegacyIcon(), - ) + ) + } + "filters" -> reader.forEach { + it + .let { + if (version < Upgrade_13_2.VERSION) + filterCriteriaProvider.rebuildFilter(it) + else + it + } + .let { filter -> + if (filterDao.getByName(filter.title!!) == null) { + filterDao.insert( + filter.copy( + color = themeToColor(context, version, filter.color ?: 0), + icon = filter.icon.migrateLegacyIcon(), ) - } + ) } - } - "caldavAccounts" -> reader.forEach { account -> - if (caldavDao.getAccountByUuid(account.uuid!!) == null) { - caldavDao.insert(account) } + } + "caldavAccounts" -> reader.forEach { account -> + if (caldavDao.getAccountByUuid(account.uuid!!) == null) { + caldavDao.insert(account) } - "caldavCalendars" -> reader.forEach { calendar -> - if (caldavDao.getCalendarByUuid(calendar.uuid!!) == null) { - caldavDao.insert( - calendar.copy( - color = themeToColor(context, version, calendar.color), - icon = calendar.icon.migrateLegacyIcon(), - ) + } + "caldavCalendars" -> reader.forEach { calendar -> + if (caldavDao.getCalendarByUuid(calendar.uuid!!) == null) { + caldavDao.insert( + calendar.copy( + color = themeToColor(context, version, calendar.color), + icon = calendar.icon.migrateLegacyIcon(), ) - } + ) } - "taskListMetadata" -> reader.forEach { tlm -> - val id = tlm.filter.takeIf { it?.isNotBlank() == true } ?: tlm.tagUuid!! - if (taskListMetadataDao.fetchByTagOrFilter(id) == null) { - taskListMetadataDao.insert(tlm) - } + } + "taskListMetadata" -> reader.forEach { tlm -> + val id = tlm.filter.takeIf { it?.isNotBlank() == true } ?: tlm.tagUuid!! + if (taskListMetadataDao.fetchByTagOrFilter(id) == null) { + taskListMetadataDao.insert(tlm) } - "taskAttachments" -> reader.forEach { attachment -> - if (taskAttachmentDao.getAttachment(attachment.remoteId) == null) { - taskAttachmentDao.insert(attachment) - } + } + "taskAttachments" -> reader.forEach { attachment -> + if (taskAttachmentDao.getAttachment(attachment.remoteId) == null) { + taskAttachmentDao.insert(attachment) } - "intPrefs" -> - Json.decodeFromString>(reader.jsonString()) - .filterNot { (key, _) -> ignoreKeys.contains(key) } - .forEach { (k, v) -> preferences.setInt(k, v as Int) } - "longPrefs" -> - Json.decodeFromString>(reader.jsonString()) - .filterNot { (key, _) -> ignoreKeys.contains(key) } - .forEach { (k, v) -> preferences.setLong(k, v as Long)} - "stringPrefs" -> - Json.decodeFromString>(reader.jsonString()) - .filterNot { (k, _) -> ignoreKeys.contains(k) } - .forEach { (k, v) -> preferences.setString(k, v)} - "boolPrefs" -> - Json.decodeFromString>(reader.jsonString()) - .filterNot { (k, _) -> ignoreKeys.contains(k) } - .forEach { (k, v) -> preferences.setBoolean(k, v as Boolean) } - "setPrefs" -> - Json.decodeFromString>>(reader.jsonString()) - .filterNot { (k, _) -> ignoreKeys.contains(k) } - .forEach { (k, v) -> preferences.setStringSet(k, v as HashSet)} - "googleTaskAccounts" -> reader.forEach { googleTaskAccount -> - if (caldavDao.getAccount(TYPE_GOOGLE_TASKS, googleTaskAccount.account!!) == null) { - caldavDao.insert( - CaldavAccount( - accountType = TYPE_GOOGLE_TASKS, - uuid = googleTaskAccount.account, - name = googleTaskAccount.account, - username = googleTaskAccount.account, - ) + } + "intPrefs" -> + Json.decodeFromString>(reader.jsonString()) + .filterNot { (key, _) -> ignoreKeys.contains(key) } + .forEach { (k, v) -> preferences.setInt(k, v as Int) } + "longPrefs" -> + Json.decodeFromString>(reader.jsonString()) + .filterNot { (key, _) -> ignoreKeys.contains(key) } + .forEach { (k, v) -> preferences.setLong(k, v as Long)} + "stringPrefs" -> + Json.decodeFromString>(reader.jsonString()) + .filterNot { (k, _) -> ignoreKeys.contains(k) } + .forEach { (k, v) -> preferences.setString(k, v)} + "boolPrefs" -> + Json.decodeFromString>(reader.jsonString()) + .filterNot { (k, _) -> ignoreKeys.contains(k) } + .forEach { (k, v) -> preferences.setBoolean(k, v as Boolean) } + "setPrefs" -> + Json.decodeFromString>>(reader.jsonString()) + .filterNot { (k, _) -> ignoreKeys.contains(k) } + .forEach { (k, v) -> preferences.setStringSet(k, v as HashSet)} + "googleTaskAccounts" -> reader.forEach { googleTaskAccount -> + if (caldavDao.getAccount(TYPE_GOOGLE_TASKS, googleTaskAccount.account!!) == null) { + caldavDao.insert( + CaldavAccount( + accountType = TYPE_GOOGLE_TASKS, + uuid = googleTaskAccount.account, + name = googleTaskAccount.account, + username = googleTaskAccount.account, ) - } + ) } - "googleTaskLists" -> reader.forEach { googleTaskList -> - if (caldavDao.getCalendar(googleTaskList.remoteId!!) == null) { - caldavDao.insert( - CaldavCalendar( - account = googleTaskList.account, - uuid = googleTaskList.remoteId, - color = themeToColor(context, version, googleTaskList.color ?: 0), - icon = googleTaskList.icon?.toString().migrateLegacyIcon(), - ) + } + "googleTaskLists" -> reader.forEach { googleTaskList -> + if (caldavDao.getCalendar(googleTaskList.remoteId!!) == null) { + caldavDao.insert( + CaldavCalendar( + account = googleTaskList.account, + uuid = googleTaskList.remoteId, + color = themeToColor(context, version, googleTaskList.color ?: 0), + icon = googleTaskList.icon?.toString().migrateLegacyIcon(), ) - } - } - else -> { - Timber.w("Skipping $element") - reader.skipValue() + ) } } + else -> { + Timber.w("Skipping $element") + reader.skipValue() + } } - reader.endObject() - } - else -> { - Timber.w("Skipping $name") - reader.skipValue() } + reader.endObject() + } + else -> { + Timber.w("Skipping $name") + reader.skipValue() } } - if (version < Upgrader.V8_2) { - val themeIndex = preferences.getInt(R.string.p_theme_color, 7) - preferences.setInt( - R.string.p_theme_color, - getAndroidColor(context, themeIndex)) - } - if (version < Upgrader.V9_6) { - taskMover.migrateLocalTasks() + } + reader.close() + bufferedReader.close() + `is`.close() + return version + } + + private suspend fun importTasks( + context: Context, + backupFile: Uri?, + progressDialog: ProgressDialog?, + version: Int, + ) { + val handler = Handler(context.mainLooper) + val `is`: InputStream? = try { + context.contentResolver.openInputStream(backupFile!!) + } catch (e: FileNotFoundException) { + throw IllegalStateException(e) + } + val bufferedReader = `is`!!.bufferedReader() + val reader = JsonReader(bufferedReader) + reader.isLenient = true + reader.beginObject() + while (reader.hasNext()) { + when (val name = reader.nextName()) { + "data" -> { + reader.beginObject() + while (reader.hasNext()) { + when (val element = reader.nextName()) { + "tasks" -> { + reader.forEach { backup -> + result.taskCount++ + setProgressMessage( + handler, + progressDialog, + context.getString(R.string.import_progress_read, result.taskCount)) + importTask(backup, version) + } + } + else -> { + Timber.w("Skipping $element") + reader.skipValue() + } + } + } + reader.endObject() + } + else -> { + Timber.w("Skipping $name") + reader.skipValue() + } } - Timber.d("Updating parents") - caldavDao.updateParents() - reader.close() - bufferedReader.close() - `is`!!.close() - } catch (e: IOException) { - Timber.e(e) } - localBroadcastManager.broadcastRefresh() - return result + reader.close() + bufferedReader.close() + `is`.close() } private suspend fun importTask(backup: TaskBackup, version: Int) {