mirror of
https://github.com/FossifyOrg/Clock.git
synced 2026-04-21 23:07:05 -04:00
Fixed data importing and made code more robust
This commit is contained in:
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(",")}]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.fossify.clock.interfaces
|
||||
|
||||
interface JSONConvertible {
|
||||
fun toJSON(): String
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user