Fixed data importing and made code more robust

This commit is contained in:
ronniedroid
2024-04-21 10:37:01 +03:00
parent 2a381a3ee6
commit 34dfd03a4b
7 changed files with 249 additions and 143 deletions

View File

@@ -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
}
)
}
}
}

View File

@@ -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<Alarm>,
timers: List<Timer>,
alarms: List<JSONConvertible>,
timers: List<JSONConvertible>,
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<Alarm>?): String {
if (alarms.isNullOrEmpty()) {
return "[]"
private fun toJsonArray(list: List<JSONConvertible>): JSONArray {
return if (list.isEmpty()) {
JSONArray()
} else {
JSONArray().apply {
list.forEach { item ->
put(JSONObject(item.toJSON()))
}
}
}
val jsonAlarms = mutableListOf<String>()
for (alarm in alarms) {
jsonAlarms.add(alarm.toJSON())
}
return "[${jsonAlarms.joinToString(",")}]"
}
private fun timersToJSON(timers: List<Timer>?): String {
if (timers.isNullOrEmpty()) {
return "[]"
}
val jsonTimers = mutableListOf<String>()
for (timer in timers) {
jsonTimers.add(timer.toJSON())
}
return "[${jsonTimers.joinToString(",")}]"
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
package org.fossify.clock.interfaces
interface JSONConvertible {
fun toJSON(): String
}

View File

@@ -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

View File

@@ -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

View File

@@ -55,7 +55,11 @@
<string name="settings_import_data_subtitle">Import app data (Alarms and Timers)</string>
<string name="settings_export_data">Export data</string>
<string name="settings_export_data_subtitle">Export app data (Alarms and Timers)</string>
<string name="exporting_aborted_by_user">Exporting aborted by user</string>
<string name="exporting_aborted">Exporting aborted</string>
<string name="importing_aborted">Importing aborted</string>
<string name="alarms_import_failed">Importing alarms failed</string>
<string name="timers_import_failed">Importing timers failed</string>
<string name="import_incomplete">Partial import due to wrong data</string>
<!-- FAQ -->
<string name="faq_1_title">How can I change lap sorting at the stopwatch tab?</string>