From 34dfd03a4bb7a526d55ccdd9c438d041614018c5 Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Sun, 21 Apr 2024 10:37:01 +0300 Subject: [PATCH] Fixed data importing and made code more robust --- .../clock/activities/SettingsActivity.kt | 165 ++++++++++-------- .../org/fossify/clock/helpers/DataExporter.kt | 53 +++--- .../org/fossify/clock/helpers/DataImporter.kt | 76 ++++---- .../clock/interfaces/JSONConvertible.kt | 5 + .../kotlin/org/fossify/clock/models/Alarm.kt | 43 ++++- .../kotlin/org/fossify/clock/models/Timer.kt | 44 ++++- app/src/main/res/values/strings.xml | 6 +- 7 files changed, 249 insertions(+), 143 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt diff --git a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt index a95f2da3..8046e92b 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -2,6 +2,7 @@ package org.fossify.clock.activities import android.content.ActivityNotFoundException import android.content.Intent +import android.net.Uri import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts @@ -10,15 +11,16 @@ import org.fossify.clock.dialogs.ExportDataDialog import org.fossify.clock.extensions.config import org.fossify.clock.extensions.dbHelper import org.fossify.clock.extensions.timerDb -import org.fossify.clock.extensions.timerHelper import org.fossify.clock.helpers.* import org.fossify.commons.R -import org.fossify.clock.R as CR +import org.fossify.commons.dialogs.FilePickerDialog import org.fossify.commons.extensions.* import org.fossify.commons.helpers.* +import java.io.FileOutputStream import java.io.OutputStream import java.util.Locale import kotlin.system.exitProcess +import org.fossify.clock.R as CR class SettingsActivity : SimpleActivity() { private val binding: ActivitySettingsBinding by viewBinding(ActivitySettingsBinding::inflate) @@ -49,6 +51,7 @@ class SettingsActivity : SimpleActivity() { setupIncreaseVolumeGradually() setupCustomizeWidgetColors() setupExportData() + setupImportData() updateTextColors(binding.settingsHolder) arrayOf( @@ -184,17 +187,31 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupImportData() { + binding.settingsImportDataHolder.setOnClickListener { + tryImportData() + } + } + private val exportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> - try { - val outputStream = uri?.let { contentResolver.openOutputStream(it) } - if (outputStream != null) { - exportDataTo(outputStream) - } else { - toast(CR.string.exporting_aborted_by_user) - } - } catch (e: Exception) { - showErrorToast(e) + try { + val outputStream = uri?.let { contentResolver.openOutputStream(it) } + if (outputStream != null) { + exportDataTo(outputStream) + } else { + toast(CR.string.exporting_aborted) } + } catch (e: Exception) { + showErrorToast(e) + } + } + + private val importActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null) { + tryImportDataFromFile(uri) + } else { + toast(CR.string.importing_aborted) + } } private fun exportDataTo(outputStream: OutputStream?) { @@ -248,61 +265,73 @@ class SettingsActivity : SimpleActivity() { } } -// private fun tryImportData() { -// if (isQPlus()) { -// Intent(Intent.ACTION_GET_CONTENT).apply { -// addCategory(Intent.CATEGORY_OPENABLE) -// type = "application/json" -// } -// } else { -// handlePermission(PERMISSION_READ_STORAGE) { isAllowed -> -// if (isAllowed) { -// pickFileToImportData() -// } -// } -// } -// } -// -// private fun pickFileToImportData() { -// FilePickerDialog(this) { -// importData(it) -// } -// } -// -// private fun tryImportDataFromFile(uri: Uri) { -// when (uri.scheme) { -// "file" -> importData(uri.path!!) -// "content" -> { -// val tempFile = getTempFile("fossify_clock_data", "fossify_clock_data.json") -// if (tempFile == null) { -// toast(R.string.unknown_error_occurred) -// return -// } -// -// try { -// val inputStream = contentResolver.openInputStream(uri) -// val out = FileOutputStream(tempFile) -// inputStream!!.copyTo(out) -// importData(tempFile.absolutePath) -// } catch (e: Exception) { -// showErrorToast(e) -// } -// } -// -// else -> toast(R.string.invalid_file_format) -// } -// } -// -// private fun importData(path: String) { -// ensureBackgroundThread { -// val result = AlarmsImporter(this, DBHelper.dbInstance!!).importAlarms(path) -// toast( -// when (result) { -// AlarmsImporter.ImportResult.IMPORT_OK -> -// R.string.importing_successful -// AlarmsImporter.ImportResult.IMPORT_FAIL -> R.string.no_items_found -// } -// ) -// } -// } + private fun tryImportData() { + if (isQPlus()) { + Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/json" + + try { + importActivityResultLauncher.launch(type) + } catch (e: ActivityNotFoundException) { + toast(R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) + } + } + } else { + handlePermission(PERMISSION_READ_STORAGE) { isAllowed -> + if (isAllowed) { + pickFileToImportData() + } + } + } + } + + private fun pickFileToImportData() { + FilePickerDialog(this) { + importData(it) + } + } + + private fun tryImportDataFromFile(uri: Uri) { + when (uri.scheme) { + "file" -> importData(uri.path!!) + "content" -> { + val tempFile = getTempFile("fossify_clock_data", "fossify_clock_data.json") + if (tempFile == null) { + toast(R.string.unknown_error_occurred) + return + } + + try { + val inputStream = contentResolver.openInputStream(uri) + val out = FileOutputStream(tempFile) + inputStream!!.copyTo(out) + importData(tempFile.absolutePath) + } catch (e: Exception) { + showErrorToast(e) + } + } + + else -> toast(R.string.invalid_file_format) + } + } + + private fun importData(path: String) { + ensureBackgroundThread { + val result = DataImporter(this, DBHelper.dbInstance!!, TimerHelper(this)).importData(path) + toast( + when (result) { + DataImporter.ImportResult.IMPORT_OK -> + R.string.importing_successful + + DataImporter.ImportResult.IMPORT_INCOMPLETE -> CR.string.import_incomplete + DataImporter.ImportResult.ALARMS_IMPORT_FAIL -> CR.string.alarms_import_failed + DataImporter.ImportResult.TIMERS_IMPORT_FAIL -> CR.string.timers_import_failed + DataImporter.ImportResult.IMPORT_FAIL -> R.string.no_items_found + } + ) + } + } } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt index fdca4fdc..43f7c626 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt @@ -1,15 +1,16 @@ package org.fossify.clock.helpers -import org.fossify.clock.models.Alarm -import org.fossify.clock.models.Timer +import org.fossify.clock.interfaces.JSONConvertible import org.fossify.commons.helpers.ExportResult +import org.json.JSONArray +import org.json.JSONObject import java.io.OutputStream object DataExporter { fun exportData( - alarms: ArrayList, - timers: List, + alarms: List, + timers: List, outputStream: OutputStream?, callback: (result: ExportResult) -> Unit, ) { @@ -18,14 +19,17 @@ object DataExporter { return } - val alarmsToExport = alarmsToJSON(alarms) - val timersToExport = timersToJSON(timers) + val alarmsJsonArray = toJsonArray(alarms) + val timersJsonArray = toJsonArray(timers) - val dataToExport = "{\"alarms\": $alarmsToExport, \"timers\": $timersToExport" + val jsonObject = JSONObject().apply { + put("alarms", alarmsJsonArray) + put("timers", timersJsonArray) + } try { outputStream.bufferedWriter().use { out -> - out.write(dataToExport) + out.write(jsonObject.toString()) } callback.invoke(ExportResult.EXPORT_OK) } catch (e: Exception) { @@ -33,30 +37,15 @@ object DataExporter { } } - // Replace with a generic later - private fun alarmsToJSON(alarms: List?): String { - if (alarms.isNullOrEmpty()) { - return "[]" + private fun toJsonArray(list: List): JSONArray { + return if (list.isEmpty()) { + JSONArray() + } else { + JSONArray().apply { + list.forEach { item -> + put(JSONObject(item.toJSON())) + } + } } - - val jsonAlarms = mutableListOf() - for (alarm in alarms) { - jsonAlarms.add(alarm.toJSON()) - } - - return "[${jsonAlarms.joinToString(",")}]" - } - - private fun timersToJSON(timers: List?): String { - if (timers.isNullOrEmpty()) { - return "[]" - } - - val jsonTimers = mutableListOf() - for (timer in timers) { - jsonTimers.add(timer.toJSON()) - } - - return "[${jsonTimers.joinToString(",")}]" } } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt index e7262eb1..4ae87958 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt @@ -2,29 +2,47 @@ package org.fossify.clock.helpers import android.app.Activity import org.fossify.clock.models.Alarm +import org.fossify.clock.models.Timer import org.fossify.commons.extensions.showErrorToast import org.json.JSONArray import org.json.JSONObject - import java.io.File class DataImporter( private val activity: Activity, private val dbHelper: DBHelper, + private val timerHelper: TimerHelper ) { + enum class ImportResult { - IMPORT_FAIL, IMPORT_OK + IMPORT_INCOMPLETE, + ALARMS_IMPORT_FAIL, + TIMERS_IMPORT_FAIL, + IMPORT_FAIL, + IMPORT_OK } fun importData(path: String): ImportResult { return try { val inputStream = File(path).inputStream() val jsonString = inputStream.bufferedReader().use { it.readText().trimEnd() } - val jsonArray = JSONArray(jsonString) + val jsonObject = JSONObject(jsonString) + val alarmsFromJson = jsonObject.getJSONArray("alarms") + val timersFromJson = jsonObject.getJSONArray("timers") - val insertedCount = insertAlarmsFromJSON(jsonArray) - if (insertedCount > 0) { - ImportResult.IMPORT_OK + val importedAlarms = insertAlarmsFromJSON(alarmsFromJson) + val importedTimers = insertTimersFromJSON(timersFromJson) + + if (importedAlarms > 0 || importedTimers > 0) { + if (importedAlarms < alarmsFromJson.length() || importedTimers < timersFromJson.length()) { + ImportResult.IMPORT_INCOMPLETE + } else { + ImportResult.IMPORT_OK + } + } else if (importedAlarms == 0) { + ImportResult.ALARMS_IMPORT_FAIL + } else if (importedTimers == 0) { + ImportResult.TIMERS_IMPORT_FAIL } else { ImportResult.IMPORT_FAIL } @@ -34,41 +52,35 @@ class DataImporter( } } + private fun insertAlarmsFromJSON(jsonArray: JSONArray): Int { var insertedCount = 0 for (i in 0 until jsonArray.length()) { val jsonObject = jsonArray.getJSONObject(i) - val alarm = parseAlarmFromJSON(jsonObject) - val insertedId = dbHelper.insertAlarm(alarm) - if (insertedId != -1) { - insertedCount++ + if (Alarm.parseFromJSON(jsonObject) != null) { + val alarm = Alarm.parseFromJSON(jsonObject) as Alarm + if (dbHelper.insertAlarm(alarm) != -1) { + insertedCount++ + } } } return insertedCount } - private fun parseAlarmFromJSON(jsonObject: JSONObject): Alarm { - val id = jsonObject.getInt("id") - val timeInMinutes = jsonObject.getInt("timeInMinutes") - val days = jsonObject.getInt("days") - val isEnabled = jsonObject.getBoolean("isEnabled") - val vibrate = jsonObject.getBoolean("vibrate") - val soundTitle = jsonObject.getString("soundTitle") - val soundUri = jsonObject.getString("soundUri") - val label = jsonObject.getString("label") - val oneShot = jsonObject.optBoolean("oneShot", false) - - return Alarm( - id, - timeInMinutes, - days, - isEnabled, - vibrate, - soundTitle, - soundUri, - label, - oneShot - ) + private fun insertTimersFromJSON(jsonArray: JSONArray): Int { + var insertedCount = 0 + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + if (Timer.parseFromJSON(jsonObject) != null) { + val timer = Timer.parseFromJSON(jsonObject) as Timer + timerHelper.insertOrUpdateTimer(timer) { id -> + if (id != -1L) { + insertedCount++ + } + } + } + } + return insertedCount } } diff --git a/app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt b/app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt new file mode 100644 index 00000000..b0977b11 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt @@ -0,0 +1,5 @@ +package org.fossify.clock.interfaces + +interface JSONConvertible { + fun toJSON(): String +} diff --git a/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt index 17b9d523..674986c5 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt @@ -1,6 +1,7 @@ package org.fossify.clock.models import androidx.annotation.Keep +import org.fossify.clock.interfaces.JSONConvertible import org.json.JSONObject @Keep @@ -14,21 +15,53 @@ data class Alarm( var soundUri: String, var label: String, var oneShot: Boolean = false, -) { - @Keep - fun toJSON(): String { +) : JSONConvertible { + override fun toJSON(): String { val jsonObject = JSONObject() jsonObject.put("id", id) jsonObject.put("timeInMinutes", timeInMinutes) jsonObject.put("days", days) - jsonObject.put("isEnabled", isEnabled) jsonObject.put("vibrate", vibrate) jsonObject.put("soundTitle", soundTitle) jsonObject.put("soundUri", soundUri) jsonObject.put("label", label) - jsonObject.put("oneShot", oneShot) return jsonObject.toString() } + + companion object { + fun parseFromJSON(jsonObject: JSONObject): Alarm? { + + if (!jsonObject.has("id") || + !jsonObject.has("timeInMinutes") || + !jsonObject.has("days") || + !jsonObject.has("vibrate") || + !jsonObject.has("soundTitle") || + !jsonObject.has("soundUri") || + !jsonObject.has("label") + ) { + return null + } + + val id = jsonObject.getInt("id") + val timeInMinutes = jsonObject.getInt("timeInMinutes") + val days = jsonObject.getInt("days") + val vibrate = jsonObject.getBoolean("vibrate") + val soundTitle = jsonObject.getString("soundTitle") + val soundUri = jsonObject.getString("soundUri") + val label = jsonObject.getString("label") + + return Alarm( + id, + timeInMinutes, + days, + false, + vibrate, + soundTitle, + soundUri, + label + ) + } + } } @Keep diff --git a/app/src/main/kotlin/org/fossify/clock/models/Timer.kt b/app/src/main/kotlin/org/fossify/clock/models/Timer.kt index 3f237049..523af0d8 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/Timer.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/Timer.kt @@ -3,6 +3,7 @@ package org.fossify.clock.models import androidx.annotation.Keep import androidx.room.Entity import androidx.room.PrimaryKey +import org.fossify.clock.interfaces.JSONConvertible import org.json.JSONObject @Entity(tableName = "timers") @@ -18,21 +19,54 @@ data class Timer( var createdAt: Long, var channelId: String? = null, var oneShot: Boolean = false, -) { - @Keep - fun toJSON(): String { +) : JSONConvertible { + override fun toJSON(): String { val jsonObject = JSONObject() jsonObject.put("id", id) - jsonObject.put("state", state) + jsonObject.put("seconds", seconds) jsonObject.put("vibrate", vibrate) jsonObject.put("soundUri", soundUri) jsonObject.put("soundTitle", soundTitle) jsonObject.put("label", label) jsonObject.put("createdAt", createdAt) - jsonObject.put("channelId", channelId) jsonObject.put("oneShot", oneShot) return jsonObject.toString() } + + companion object { + fun parseFromJSON(jsonObject: JSONObject): Timer? { + + if (!jsonObject.has("id") || + !jsonObject.has("seconds") || + !jsonObject.has("vibrate") || + !jsonObject.has("soundUri") || + !jsonObject.has("soundTitle") || + !jsonObject.has("label") || + !jsonObject.has("createdAt") + ) { + return null + } + + val id = jsonObject.getInt("id") + val second = jsonObject.getInt("seconds") + val vibrate = jsonObject.getBoolean("vibrate") + val soundUri = jsonObject.getString("soundUri") + val soundTitle = jsonObject.getString("soundTitle") + val label = jsonObject.getString("label") + val createdAt = jsonObject.getLong("createdAt") + + return Timer( + id, + second, + TimerState.Idle, + vibrate, + soundUri, + soundTitle, + label, + createdAt + ) + } + } } @Keep diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7067627..9d9385fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,7 +55,11 @@ Import app data (Alarms and Timers) Export data Export app data (Alarms and Timers) - Exporting aborted by user + Exporting aborted + Importing aborted + Importing alarms failed + Importing timers failed + Partial import due to wrong data How can I change lap sorting at the stopwatch tab?