From b4503fc41d46ea18062ce5a7dabbf9cee46cc4e7 Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Tue, 2 Apr 2024 11:30:15 +0300 Subject: [PATCH 01/15] FEAT: added alram export and import functionality --- .../fossify/clock/activities/MainActivity.kt | 148 +++++++++++++++++- .../clock/dialogs/ExportAlarmsDialog.kt | 72 +++++++++ .../fossify/clock/helpers/AlarmsExporter.kt | 43 +++++ .../fossify/clock/helpers/AlarmsImporter.kt | 74 +++++++++ .../org/fossify/clock/helpers/Config.kt | 4 + .../org/fossify/clock/helpers/Constants.kt | 2 + .../kotlin/org/fossify/clock/models/Alarm.kt | 18 ++- .../main/res/layout/dialog_export_alarms.xml | 51 ++++++ app/src/main/res/menu/menu.xml | 8 + app/src/main/res/values/strings.xml | 3 +- 10 files changed, 415 insertions(+), 8 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt create mode 100644 app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt create mode 100644 app/src/main/kotlin/org/fossify/clock/helpers/AlarmsImporter.kt create mode 100644 app/src/main/res/layout/dialog_export_alarms.xml diff --git a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt index 6f33aa8d..48bf8bb2 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt @@ -1,27 +1,31 @@ package org.fossify.clock.activities import android.annotation.SuppressLint +import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ShortcutInfo import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable +import android.net.Uri import android.os.Bundle import android.view.WindowManager +import android.widget.Toast import me.grantland.widget.AutofitHelper import org.fossify.clock.BuildConfig import org.fossify.clock.R import org.fossify.clock.adapters.ViewPagerAdapter import org.fossify.clock.databinding.ActivityMainBinding -import org.fossify.clock.extensions.config -import org.fossify.clock.extensions.getEnabledAlarms -import org.fossify.clock.extensions.rescheduleEnabledAlarms -import org.fossify.clock.extensions.updateWidgets +import org.fossify.clock.extensions.* import org.fossify.clock.helpers.* import org.fossify.commons.databinding.BottomTablayoutItemBinding import org.fossify.commons.extensions.* import org.fossify.commons.helpers.* import org.fossify.commons.models.FAQItem +import org.fossify.clock.dialogs.ExportAlarmsDialog +import org.fossify.commons.dialogs.FilePickerDialog +import java.io.FileOutputStream +import java.io.OutputStream class MainActivity : SimpleActivity() { private var storedTextColor = 0 @@ -29,6 +33,11 @@ class MainActivity : SimpleActivity() { private var storedPrimaryColor = 0 private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::inflate) + private companion object { + private const val PICK_IMPORT_SOURCE_INTENT = 11 + private const val PICK_EXPORT_FILE_INTENT = 21 + } + override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true super.onCreate(savedInstanceState) @@ -133,6 +142,8 @@ class MainActivity : SimpleActivity() { R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.settings -> launchSettings() R.id.about -> launchAbout() + R.id.export_alarms -> tryExportAlarms() + R.id.import_alarms -> tryImportAlarms() else -> return@setOnMenuItemClickListener false } return@setOnMenuItemClickListener true @@ -142,6 +153,8 @@ class MainActivity : SimpleActivity() { private fun refreshMenuItems() { binding.mainToolbar.menu.apply { findItem(R.id.sort).isVisible = binding.viewPager.currentItem == TAB_ALARM + findItem(R.id.export_alarms).isVisible = binding.viewPager.currentItem == TAB_ALARM + findItem(R.id.import_alarms).isVisible = binding.viewPager.currentItem == TAB_ALARM findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations) } } @@ -171,8 +184,17 @@ class MainActivity : SimpleActivity() { override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { super.onActivityResult(requestCode, resultCode, resultData) - if (requestCode == PICK_AUDIO_FILE_INTENT_ID && resultCode == RESULT_OK && resultData != null) { - storeNewAlarmSound(resultData) + when { + requestCode == PICK_AUDIO_FILE_INTENT_ID && resultCode == RESULT_OK && resultData != null -> { + storeNewAlarmSound(resultData) + } + requestCode == PICK_EXPORT_FILE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null -> { + val outputStream = contentResolver.openOutputStream(resultData.data!!) + exportAlarmsTo(outputStream) + } + requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null -> { + tryImportAlarmsFromFile(resultData.data!!) + } } } @@ -298,4 +320,118 @@ class MainActivity : SimpleActivity() { startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true) } + + private fun exportAlarmsTo(outputStream: OutputStream?) { + ensureBackgroundThread { + val alarms = dbHelper.getAlarms() + if (alarms.isEmpty()) { + toast(org.fossify.commons.R.string.no_entries_for_exporting) + } else { + AlarmsExporter.exportAlarms(alarms, outputStream) { + toast( + when (it) { + ExportResult.EXPORT_OK -> org.fossify.commons.R.string.exporting_successful + else -> org.fossify.commons.R.string.exporting_failed + } + ) + } + } + } + } + + private fun tryExportAlarms() { + if (isQPlus()) { + ExportAlarmsDialog(this, config.lastAlarmsExportPath, true) { file -> + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + type = "application/json" + putExtra(Intent.EXTRA_TITLE, file.name) + addCategory(Intent.CATEGORY_OPENABLE) + + try { + startActivityForResult(this, PICK_EXPORT_FILE_INTENT) + } catch (e: ActivityNotFoundException) { + toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) + } + } + } + } else { + handlePermission(PERMISSION_WRITE_STORAGE) { isAllowed -> + if (isAllowed) { + ExportAlarmsDialog(this, config.lastAlarmsExportPath, false) { file -> + getFileOutputStream(file.toFileDirItem(this), true) { out -> + exportAlarmsTo(out) + } + } + } + } + } + } + + private fun tryImportAlarms() { + if (isQPlus()) { + Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/json" + + try { + startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT) + } catch (e: ActivityNotFoundException) { + toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) + } + } + } else { + handlePermission(PERMISSION_READ_STORAGE) { isAllowed -> + if (isAllowed) { + pickFileToImportAlarms() + } + } + } + } + + private fun pickFileToImportAlarms() { + FilePickerDialog(this) { + importAlarms(it) + } + } + + private fun tryImportAlarmsFromFile(uri: Uri) { + when (uri.scheme) { + "file" -> importAlarms(uri.path!!) + "content" -> { + val tempFile = getTempFile("alarms", "alarms.json") + if (tempFile == null) { + toast(org.fossify.commons.R.string.unknown_error_occurred) + return + } + + try { + val inputStream = contentResolver.openInputStream(uri) + val out = FileOutputStream(tempFile) + inputStream!!.copyTo(out) + importAlarms(tempFile.absolutePath) + } catch (e: Exception) { + showErrorToast(e) + } + } + + else -> toast(org.fossify.commons.R.string.invalid_file_format) + } + } + + private fun importAlarms(path: String) { + ensureBackgroundThread { + val result = AlarmsImporter(this, DBHelper.dbInstance!!).importAlarms(path) + toast( + when (result) { + AlarmsImporter.ImportResult.IMPORT_OK -> + org.fossify.commons.R.string.importing_successful + AlarmsImporter.ImportResult.IMPORT_FAIL -> org.fossify.commons.R.string.no_items_found + } + ) + } + } } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt new file mode 100644 index 00000000..de48d2e4 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt @@ -0,0 +1,72 @@ +package org.fossify.clock.dialogs + +import androidx.appcompat.app.AlertDialog +import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.dialogs.FilePickerDialog +import org.fossify.commons.extensions.* +import org.fossify.commons.helpers.ensureBackgroundThread +import org.fossify.clock.R +import org.fossify.clock.databinding.DialogExportAlarmsBinding +import org.fossify.clock.extensions.config +import org.fossify.clock.helpers.ALARMS_EXPORT_EXTENSION +import java.io.File + +class ExportAlarmsDialog( + val activity: BaseSimpleActivity, + val path: String, + val hidePath: Boolean, + callback: (file: File) -> Unit, +) { + private var realPath = path.ifEmpty { activity.internalStoragePath } + private val config = activity.config + + init { + val view = DialogExportAlarmsBinding.inflate(activity.layoutInflater, null, false).apply { + exportAlarmsFolder.text = activity.humanizePath(realPath) + exportAlarmsFilename.setText("${activity.getString(R.string.export_alarms)}_${activity.getCurrentFormattedDateTime()}") + + if (hidePath) { + exportAlarmsFolderLabel.beGone() + exportAlarmsFolder.beGone() + } else { + exportAlarmsFolder.setOnClickListener { + FilePickerDialog(activity, realPath, false, showFAB = true) { + exportAlarmsFolder.text = activity.humanizePath(it) + realPath = it + } + } + } + } + + activity.getAlertDialogBuilder() + .setPositiveButton(org.fossify.commons.R.string.ok, null) + .setNegativeButton(org.fossify.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(view.root, this, R.string.export_alarms) { alertDialog -> + alertDialog.showKeyboard(view.exportAlarmsFilename) + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val filename = view.exportAlarmsFilename.value + when { + filename.isEmpty() -> activity.toast(org.fossify.commons.R.string.empty_name) + filename.isAValidFilename() -> { + val file = File(realPath, "$filename$ALARMS_EXPORT_EXTENSION") + if (!hidePath && file.exists()) { + activity.toast(org.fossify.commons.R.string.name_taken) + return@setOnClickListener + } + + ensureBackgroundThread { + config.lastAlarmsExportPath = file.absolutePath.getParentPath() + callback(file) + alertDialog.dismiss() + } + } + + else -> activity.toast(org.fossify.commons.R.string.invalid_name) + } + } + } + } + } +} + diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt new file mode 100644 index 00000000..28e4896e --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt @@ -0,0 +1,43 @@ +package org.fossify.clock.helpers + +import org.fossify.clock.models.Alarm +import org.fossify.commons.helpers.ExportResult +import java.io.OutputStream + +object AlarmsExporter { + fun exportAlarms( + alarms: ArrayList, + outputStream: OutputStream?, + callback: (result: ExportResult) -> Unit, + ) { + if (outputStream == null) { + callback.invoke(ExportResult.EXPORT_FAIL) + return + } + + val alarmsToExport = alarmsToJSON(alarms) + + try { + outputStream.bufferedWriter().use { out -> + out.write(alarmsToExport) + } + callback.invoke(ExportResult.EXPORT_OK) + } catch (e: Exception) { + callback.invoke(ExportResult.EXPORT_FAIL) + } + } + + private fun alarmsToJSON(alarms: List?): String { + if (alarms.isNullOrEmpty()) { + return "[]" + } + + val jsonAlarms = mutableListOf() + for (alarm in alarms) { + jsonAlarms.add(alarm.toJSON()) + } + + return "[${jsonAlarms.joinToString(",")}]" + } + +} diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsImporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsImporter.kt new file mode 100644 index 00000000..7c97f8fd --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsImporter.kt @@ -0,0 +1,74 @@ +package org.fossify.clock.helpers + +import android.app.Activity +import org.fossify.clock.models.Alarm +import org.fossify.commons.extensions.showErrorToast +import org.json.JSONArray +import org.json.JSONObject + +import java.io.File + +class AlarmsImporter( + private val activity: Activity, + private val dbHelper: DBHelper, +) { + enum class ImportResult { + IMPORT_FAIL, IMPORT_OK + } + + fun importAlarms(path: String): ImportResult { + return try { + val inputStream = File(path).inputStream() + val jsonString = inputStream.bufferedReader().use { it.readText().trimEnd() } + val jsonArray = JSONArray(jsonString) + + val insertedCount = insertAlarmsFromJSON(jsonArray) + if (insertedCount > 0) { + ImportResult.IMPORT_OK + } else { + ImportResult.IMPORT_FAIL + } + } catch (e: Exception) { + activity.showErrorToast(e) + ImportResult.IMPORT_FAIL + } + } + + 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++ + } + } + 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 + ) + } +} + diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt index 2dbf8cdc..94d7a75c 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt @@ -104,4 +104,8 @@ class Config(context: Context) : BaseConfig(context) { var wasInitialWidgetSetUp: Boolean get() = prefs.getBoolean(WAS_INITIAL_WIDGET_SET_UP, false) set(wasInitialWidgetSetUp) = prefs.edit().putBoolean(WAS_INITIAL_WIDGET_SET_UP, wasInitialWidgetSetUp).apply() + + var lastAlarmsExportPath: String + get() = prefs.getString(LAST_ALARMS_EXPORT_PATH, "")!! + set(lastBlockedNumbersExportPath) = prefs.edit().putString(LAST_ALARMS_EXPORT_PATH, lastBlockedNumbersExportPath).apply() } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt index 99fd99c4..214926cc 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt @@ -26,6 +26,8 @@ const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually" const val ALARMS_SORT_BY = "alarms_sort_by" const val STOPWATCH_LAPS_SORT_BY = "stopwatch_laps_sort_by" const val WAS_INITIAL_WIDGET_SET_UP = "was_initial_widget_set_up" +const val ALARMS_EXPORT_EXTENSION = ".json" +const val LAST_ALARMS_EXPORT_PATH = "last_alarms_export_path" const val TABS_COUNT = 4 const val EDITED_TIME_ZONE_SEPARATOR = ":" 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 fc939900..17b9d523 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.json.JSONObject @Keep data class Alarm( @@ -13,7 +14,22 @@ data class Alarm( var soundUri: String, var label: String, var oneShot: Boolean = false, -) +) { + @Keep + 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() + } +} @Keep data class ObfuscatedAlarm( diff --git a/app/src/main/res/layout/dialog_export_alarms.xml b/app/src/main/res/layout/dialog_export_alarms.xml new file mode 100644 index 00000000..dac0d742 --- /dev/null +++ b/app/src/main/res/layout/dialog_export_alarms.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index a3db8c50..be670e05 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -11,6 +11,14 @@ android:icon="@drawable/ic_settings_cog_vector" android:title="@string/settings" app:showAsAction="ifRoom" /> + + Add timer Upcoming alarm Early alarm dismissal - + Import alarms + Export alarms Timers are running Timer for %s is running From 2a381a3ee6bf3dd3a06de12c2abc055cc502628a Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Thu, 18 Apr 2024 13:05:54 +0300 Subject: [PATCH 02/15] moved to registerForActivityResult and added timers to exports --- .../fossify/clock/activities/MainActivity.kt | 137 ---------------- .../clock/activities/SettingsActivity.kt | 147 +++++++++++++++++- ...ortAlarmsDialog.kt => ExportDataDialog.kt} | 30 ++-- .../org/fossify/clock/helpers/Config.kt | 6 +- .../org/fossify/clock/helpers/Constants.kt | 4 +- .../{AlarmsExporter.kt => DataExporter.kt} | 25 ++- .../{AlarmsImporter.kt => DataImporter.kt} | 4 +- .../kotlin/org/fossify/clock/models/Timer.kt | 18 ++- app/src/main/res/layout/activity_settings.xml | 57 +++++++ ...port_alarms.xml => dialog_export_data.xml} | 10 +- app/src/main/res/menu/menu.xml | 8 - app/src/main/res/values/strings.xml | 9 +- 12 files changed, 271 insertions(+), 184 deletions(-) rename app/src/main/kotlin/org/fossify/clock/dialogs/{ExportAlarmsDialog.kt => ExportDataDialog.kt} (68%) rename app/src/main/kotlin/org/fossify/clock/helpers/{AlarmsExporter.kt => DataExporter.kt} (61%) rename app/src/main/kotlin/org/fossify/clock/helpers/{AlarmsImporter.kt => DataImporter.kt} (96%) rename app/src/main/res/layout/{dialog_export_alarms.xml => dialog_export_data.xml} (88%) diff --git a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt index 48bf8bb2..cd03f294 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt @@ -1,16 +1,13 @@ package org.fossify.clock.activities import android.annotation.SuppressLint -import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ShortcutInfo import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable -import android.net.Uri import android.os.Bundle import android.view.WindowManager -import android.widget.Toast import me.grantland.widget.AutofitHelper import org.fossify.clock.BuildConfig import org.fossify.clock.R @@ -22,10 +19,6 @@ import org.fossify.commons.databinding.BottomTablayoutItemBinding import org.fossify.commons.extensions.* import org.fossify.commons.helpers.* import org.fossify.commons.models.FAQItem -import org.fossify.clock.dialogs.ExportAlarmsDialog -import org.fossify.commons.dialogs.FilePickerDialog -import java.io.FileOutputStream -import java.io.OutputStream class MainActivity : SimpleActivity() { private var storedTextColor = 0 @@ -33,11 +26,6 @@ class MainActivity : SimpleActivity() { private var storedPrimaryColor = 0 private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::inflate) - private companion object { - private const val PICK_IMPORT_SOURCE_INTENT = 11 - private const val PICK_EXPORT_FILE_INTENT = 21 - } - override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true super.onCreate(savedInstanceState) @@ -142,8 +130,6 @@ class MainActivity : SimpleActivity() { R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.settings -> launchSettings() R.id.about -> launchAbout() - R.id.export_alarms -> tryExportAlarms() - R.id.import_alarms -> tryImportAlarms() else -> return@setOnMenuItemClickListener false } return@setOnMenuItemClickListener true @@ -153,8 +139,6 @@ class MainActivity : SimpleActivity() { private fun refreshMenuItems() { binding.mainToolbar.menu.apply { findItem(R.id.sort).isVisible = binding.viewPager.currentItem == TAB_ALARM - findItem(R.id.export_alarms).isVisible = binding.viewPager.currentItem == TAB_ALARM - findItem(R.id.import_alarms).isVisible = binding.viewPager.currentItem == TAB_ALARM findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations) } } @@ -188,13 +172,6 @@ class MainActivity : SimpleActivity() { requestCode == PICK_AUDIO_FILE_INTENT_ID && resultCode == RESULT_OK && resultData != null -> { storeNewAlarmSound(resultData) } - requestCode == PICK_EXPORT_FILE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null -> { - val outputStream = contentResolver.openOutputStream(resultData.data!!) - exportAlarmsTo(outputStream) - } - requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null -> { - tryImportAlarmsFromFile(resultData.data!!) - } } } @@ -320,118 +297,4 @@ class MainActivity : SimpleActivity() { startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true) } - - private fun exportAlarmsTo(outputStream: OutputStream?) { - ensureBackgroundThread { - val alarms = dbHelper.getAlarms() - if (alarms.isEmpty()) { - toast(org.fossify.commons.R.string.no_entries_for_exporting) - } else { - AlarmsExporter.exportAlarms(alarms, outputStream) { - toast( - when (it) { - ExportResult.EXPORT_OK -> org.fossify.commons.R.string.exporting_successful - else -> org.fossify.commons.R.string.exporting_failed - } - ) - } - } - } - } - - private fun tryExportAlarms() { - if (isQPlus()) { - ExportAlarmsDialog(this, config.lastAlarmsExportPath, true) { file -> - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - type = "application/json" - putExtra(Intent.EXTRA_TITLE, file.name) - addCategory(Intent.CATEGORY_OPENABLE) - - try { - startActivityForResult(this, PICK_EXPORT_FILE_INTENT) - } catch (e: ActivityNotFoundException) { - toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) - } catch (e: Exception) { - showErrorToast(e) - } - } - } - } else { - handlePermission(PERMISSION_WRITE_STORAGE) { isAllowed -> - if (isAllowed) { - ExportAlarmsDialog(this, config.lastAlarmsExportPath, false) { file -> - getFileOutputStream(file.toFileDirItem(this), true) { out -> - exportAlarmsTo(out) - } - } - } - } - } - } - - private fun tryImportAlarms() { - if (isQPlus()) { - Intent(Intent.ACTION_GET_CONTENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/json" - - try { - startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT) - } catch (e: ActivityNotFoundException) { - toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) - } catch (e: Exception) { - showErrorToast(e) - } - } - } else { - handlePermission(PERMISSION_READ_STORAGE) { isAllowed -> - if (isAllowed) { - pickFileToImportAlarms() - } - } - } - } - - private fun pickFileToImportAlarms() { - FilePickerDialog(this) { - importAlarms(it) - } - } - - private fun tryImportAlarmsFromFile(uri: Uri) { - when (uri.scheme) { - "file" -> importAlarms(uri.path!!) - "content" -> { - val tempFile = getTempFile("alarms", "alarms.json") - if (tempFile == null) { - toast(org.fossify.commons.R.string.unknown_error_occurred) - return - } - - try { - val inputStream = contentResolver.openInputStream(uri) - val out = FileOutputStream(tempFile) - inputStream!!.copyTo(out) - importAlarms(tempFile.absolutePath) - } catch (e: Exception) { - showErrorToast(e) - } - } - - else -> toast(org.fossify.commons.R.string.invalid_file_format) - } - } - - private fun importAlarms(path: String) { - ensureBackgroundThread { - val result = AlarmsImporter(this, DBHelper.dbInstance!!).importAlarms(path) - toast( - when (result) { - AlarmsImporter.ImportResult.IMPORT_OK -> - org.fossify.commons.R.string.importing_successful - AlarmsImporter.ImportResult.IMPORT_FAIL -> org.fossify.commons.R.string.no_items_found - } - ) - } - } } 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 79413d6b..a95f2da3 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -1,16 +1,22 @@ package org.fossify.clock.activities +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import org.fossify.clock.databinding.ActivitySettingsBinding +import org.fossify.clock.dialogs.ExportDataDialog import org.fossify.clock.extensions.config -import org.fossify.clock.helpers.DEFAULT_MAX_ALARM_REMINDER_SECS -import org.fossify.clock.helpers.DEFAULT_MAX_TIMER_REMINDER_SECS +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.extensions.* -import org.fossify.commons.helpers.IS_CUSTOMIZING_COLORS -import org.fossify.commons.helpers.MINUTE_SECONDS -import org.fossify.commons.helpers.NavigationIcon -import org.fossify.commons.helpers.isTiramisuPlus +import org.fossify.commons.helpers.* +import java.io.OutputStream import java.util.Locale import kotlin.system.exitProcess @@ -42,6 +48,7 @@ class SettingsActivity : SimpleActivity() { setupTimerMaxReminder() setupIncreaseVolumeGradually() setupCustomizeWidgetColors() + setupExportData() updateTextColors(binding.settingsHolder) arrayOf( @@ -170,4 +177,132 @@ class SettingsActivity : SimpleActivity() { } } } + + private fun setupExportData() { + binding.settingsExportDataHolder.setOnClickListener { + tryExportData() + } + } + + 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) + } + } + + private fun exportDataTo(outputStream: OutputStream?) { + ensureBackgroundThread { + val alarms = dbHelper.getAlarms() + val timers = timerDb.getTimers() + if (alarms.isEmpty()) { + toast(R.string.no_entries_for_exporting) + } else { + DataExporter.exportData(alarms, timers, outputStream) { + toast( + when (it) { + ExportResult.EXPORT_OK -> R.string.exporting_successful + else -> R.string.exporting_failed + } + ) + } + } + } + } + + private fun tryExportData() { + if (isQPlus()) { + ExportDataDialog(this, config.lastDataExportPath, true) { file -> + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + putExtra(Intent.EXTRA_TITLE, file.name) + addCategory(Intent.CATEGORY_OPENABLE) + + + try { + exportActivityResultLauncher.launch(file.name) + } catch (e: ActivityNotFoundException) { + toast(R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) + } + + + } + } + } else { + handlePermission(PERMISSION_WRITE_STORAGE) { isAllowed -> + if (isAllowed) { + ExportDataDialog(this, config.lastDataExportPath, false) { file -> + getFileOutputStream(file.toFileDirItem(this), true) { out -> + exportDataTo(out) + } + } + } + } + } + } + +// 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 +// } +// ) +// } +// } } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt similarity index 68% rename from app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt rename to app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt index de48d2e4..6ab79fb9 100644 --- a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportAlarmsDialog.kt +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt @@ -6,12 +6,12 @@ import org.fossify.commons.dialogs.FilePickerDialog import org.fossify.commons.extensions.* import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.clock.R -import org.fossify.clock.databinding.DialogExportAlarmsBinding +import org.fossify.clock.databinding.DialogExportDataBinding import org.fossify.clock.extensions.config -import org.fossify.clock.helpers.ALARMS_EXPORT_EXTENSION +import org.fossify.clock.helpers.DATA_EXPORT_EXTENSION import java.io.File -class ExportAlarmsDialog( +class ExportDataDialog( val activity: BaseSimpleActivity, val path: String, val hidePath: Boolean, @@ -21,17 +21,17 @@ class ExportAlarmsDialog( private val config = activity.config init { - val view = DialogExportAlarmsBinding.inflate(activity.layoutInflater, null, false).apply { - exportAlarmsFolder.text = activity.humanizePath(realPath) - exportAlarmsFilename.setText("${activity.getString(R.string.export_alarms)}_${activity.getCurrentFormattedDateTime()}") + val view = DialogExportDataBinding.inflate(activity.layoutInflater, null, false).apply { + exportDataFolder.text = activity.humanizePath(realPath) + exportDataFilename.setText("${activity.getString(R.string.settings_export_data)}_${activity.getCurrentFormattedDateTime()}") if (hidePath) { - exportAlarmsFolderLabel.beGone() - exportAlarmsFolder.beGone() + exportDataFolderLabel.beGone() + exportDataFolder.beGone() } else { - exportAlarmsFolder.setOnClickListener { + exportDataFolder.setOnClickListener { FilePickerDialog(activity, realPath, false, showFAB = true) { - exportAlarmsFolder.text = activity.humanizePath(it) + exportDataFolder.text = activity.humanizePath(it) realPath = it } } @@ -42,21 +42,21 @@ class ExportAlarmsDialog( .setPositiveButton(org.fossify.commons.R.string.ok, null) .setNegativeButton(org.fossify.commons.R.string.cancel, null) .apply { - activity.setupDialogStuff(view.root, this, R.string.export_alarms) { alertDialog -> - alertDialog.showKeyboard(view.exportAlarmsFilename) + activity.setupDialogStuff(view.root, this, R.string.settings_export_data) { alertDialog -> + alertDialog.showKeyboard(view.exportDataFilename) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - val filename = view.exportAlarmsFilename.value + val filename = view.exportDataFilename.value when { filename.isEmpty() -> activity.toast(org.fossify.commons.R.string.empty_name) filename.isAValidFilename() -> { - val file = File(realPath, "$filename$ALARMS_EXPORT_EXTENSION") + val file = File(realPath, "$filename$DATA_EXPORT_EXTENSION") if (!hidePath && file.exists()) { activity.toast(org.fossify.commons.R.string.name_taken) return@setOnClickListener } ensureBackgroundThread { - config.lastAlarmsExportPath = file.absolutePath.getParentPath() + config.lastDataExportPath = file.absolutePath.getParentPath() callback(file) alertDialog.dismiss() } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt index 94d7a75c..f069d578 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt @@ -105,7 +105,7 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getBoolean(WAS_INITIAL_WIDGET_SET_UP, false) set(wasInitialWidgetSetUp) = prefs.edit().putBoolean(WAS_INITIAL_WIDGET_SET_UP, wasInitialWidgetSetUp).apply() - var lastAlarmsExportPath: String - get() = prefs.getString(LAST_ALARMS_EXPORT_PATH, "")!! - set(lastBlockedNumbersExportPath) = prefs.edit().putString(LAST_ALARMS_EXPORT_PATH, lastBlockedNumbersExportPath).apply() + var lastDataExportPath: String + get() = prefs.getString(LAST_DATA_EXPORT_PATH, "")!! + set(lastDataExportPath) = prefs.edit().putString(LAST_DATA_EXPORT_PATH, lastDataExportPath).apply() } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt index 214926cc..9bf9748e 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt @@ -26,8 +26,8 @@ const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually" const val ALARMS_SORT_BY = "alarms_sort_by" const val STOPWATCH_LAPS_SORT_BY = "stopwatch_laps_sort_by" const val WAS_INITIAL_WIDGET_SET_UP = "was_initial_widget_set_up" -const val ALARMS_EXPORT_EXTENSION = ".json" -const val LAST_ALARMS_EXPORT_PATH = "last_alarms_export_path" +const val DATA_EXPORT_EXTENSION = ".json" +const val LAST_DATA_EXPORT_PATH = "last_alarms_export_path" const val TABS_COUNT = 4 const val EDITED_TIME_ZONE_SEPARATOR = ":" diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt similarity index 61% rename from app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt rename to app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt index 28e4896e..fdca4fdc 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsExporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt @@ -1,12 +1,15 @@ package org.fossify.clock.helpers import org.fossify.clock.models.Alarm +import org.fossify.clock.models.Timer import org.fossify.commons.helpers.ExportResult import java.io.OutputStream -object AlarmsExporter { - fun exportAlarms( +object DataExporter { + + fun exportData( alarms: ArrayList, + timers: List, outputStream: OutputStream?, callback: (result: ExportResult) -> Unit, ) { @@ -16,10 +19,13 @@ object AlarmsExporter { } val alarmsToExport = alarmsToJSON(alarms) + val timersToExport = timersToJSON(timers) + + val dataToExport = "{\"alarms\": $alarmsToExport, \"timers\": $timersToExport" try { outputStream.bufferedWriter().use { out -> - out.write(alarmsToExport) + out.write(dataToExport) } callback.invoke(ExportResult.EXPORT_OK) } catch (e: Exception) { @@ -27,6 +33,7 @@ object AlarmsExporter { } } + // Replace with a generic later private fun alarmsToJSON(alarms: List?): String { if (alarms.isNullOrEmpty()) { return "[]" @@ -40,4 +47,16 @@ object AlarmsExporter { 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/AlarmsImporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt similarity index 96% rename from app/src/main/kotlin/org/fossify/clock/helpers/AlarmsImporter.kt rename to app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt index 7c97f8fd..e7262eb1 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmsImporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt @@ -8,7 +8,7 @@ import org.json.JSONObject import java.io.File -class AlarmsImporter( +class DataImporter( private val activity: Activity, private val dbHelper: DBHelper, ) { @@ -16,7 +16,7 @@ class AlarmsImporter( IMPORT_FAIL, IMPORT_OK } - fun importAlarms(path: String): ImportResult { + fun importData(path: String): ImportResult { return try { val inputStream = File(path).inputStream() val jsonString = inputStream.bufferedReader().use { it.readText().trimEnd() } 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 f20dbe18..3f237049 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.json.JSONObject @Entity(tableName = "timers") @Keep @@ -17,7 +18,22 @@ data class Timer( var createdAt: Long, var channelId: String? = null, var oneShot: Boolean = false, -) +) { + @Keep + fun toJSON(): String { + val jsonObject = JSONObject() + jsonObject.put("id", id) + jsonObject.put("state", state) + 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() + } +} @Keep data class ObfuscatedTimer( diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 159fb86b..dba596ab 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -283,6 +283,63 @@ tools:text="1 minute" /> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_export_alarms.xml b/app/src/main/res/layout/dialog_export_data.xml similarity index 88% rename from app/src/main/res/layout/dialog_export_alarms.xml rename to app/src/main/res/layout/dialog_export_data.xml index dac0d742..f85c98c0 100644 --- a/app/src/main/res/layout/dialog_export_alarms.xml +++ b/app/src/main/res/layout/dialog_export_data.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> - - Add timer Upcoming alarm Early alarm dismissal - Import alarms - Export alarms + Timers are running Timer for %s is running @@ -51,6 +50,12 @@ Timer tab Show seconds Increase volume gradually + Export and import + Import data + Import app data (Alarms and Timers) + Export data + Export app data (Alarms and Timers) + Exporting aborted by user How can I change lap sorting at the stopwatch tab? From 34dfd03a4bb7a526d55ccdd9c438d041614018c5 Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Sun, 21 Apr 2024 10:37:01 +0300 Subject: [PATCH 03/15] 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 "[]" - } - - 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()) + private fun toJsonArray(list: List): JSONArray { + return if (list.isEmpty()) { + JSONArray() + } else { + JSONArray().apply { + list.forEach { item -> + put(JSONObject(item.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 importedAlarms = insertAlarmsFromJSON(alarmsFromJson) + val importedTimers = insertTimersFromJSON(timersFromJson) - val insertedCount = insertAlarmsFromJSON(jsonArray) - if (insertedCount > 0) { - ImportResult.IMPORT_OK + 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? From e253c564ab758ea9eb3c2ef5397c462aa47c698b Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Sun, 21 Apr 2024 11:43:10 +0300 Subject: [PATCH 04/15] Updated strings and remove action aborted toast messages --- .../clock/activities/SettingsActivity.kt | 14 ++++---- app/src/main/res/layout/activity_settings.xml | 34 +++++-------------- app/src/main/res/values/strings.xml | 10 ++---- 3 files changed, 18 insertions(+), 40 deletions(-) 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 8046e92b..5b7334f1 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -198,8 +198,6 @@ class SettingsActivity : SimpleActivity() { val outputStream = uri?.let { contentResolver.openOutputStream(it) } if (outputStream != null) { exportDataTo(outputStream) - } else { - toast(CR.string.exporting_aborted) } } catch (e: Exception) { showErrorToast(e) @@ -207,10 +205,12 @@ class SettingsActivity : SimpleActivity() { } private val importActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> - if (uri != null) { - tryImportDataFromFile(uri) - } else { - toast(CR.string.importing_aborted) + try { + if (uri != null) { + tryImportDataFromFile(uri) + } + } catch (e: Exception) { + showErrorToast(e) } } @@ -326,7 +326,7 @@ class SettingsActivity : SimpleActivity() { DataImporter.ImportResult.IMPORT_OK -> R.string.importing_successful - DataImporter.ImportResult.IMPORT_INCOMPLETE -> CR.string.import_incomplete + DataImporter.ImportResult.IMPORT_INCOMPLETE -> R.string.importing_some_entries_failed 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/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index dba596ab..0eeaaba5 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -284,16 +284,16 @@ - + - + - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d9385fc..c6fc3b41 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,16 +50,10 @@ Timer tab Show seconds Increase volume gradually - Export and import - Import data - Import app data (Alarms and Timers) - Export data - Export app data (Alarms and Timers) - Exporting aborted - Importing aborted + Import alarms and timers + Export alarms and timers Importing alarms failed Importing timers failed - Partial import due to wrong data How can I change lap sorting at the stopwatch tab? From eded923f5ed37274e70d2d7c9d2b5162f91d166f Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Mon, 22 Apr 2024 09:59:09 +0300 Subject: [PATCH 05/15] fixed doublicate alarms and replacing timers when importing --- .../org/fossify/clock/helpers/DataImporter.kt | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) 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 4ae87958..cf68ac76 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt @@ -54,13 +54,16 @@ class DataImporter( private fun insertAlarmsFromJSON(jsonArray: JSONArray): Int { + val existingAlarms = dbHelper.getAlarms() var insertedCount = 0 for (i in 0 until jsonArray.length()) { val jsonObject = jsonArray.getJSONObject(i) if (Alarm.parseFromJSON(jsonObject) != null) { val alarm = Alarm.parseFromJSON(jsonObject) as Alarm - if (dbHelper.insertAlarm(alarm) != -1) { - insertedCount++ + if (!isAlarmAlreadyInserted(alarm, existingAlarms)) { + if (dbHelper.insertAlarm(alarm) != -1) { + insertedCount++ + } } } } @@ -73,14 +76,50 @@ class DataImporter( 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++ + timerHelper.getTimers { existingTimers -> + timer.id = existingTimers.last().id?.plus(1) + if (!isTimerAlreadyInserted(timer, existingTimers)) { + timerHelper.insertOrUpdateTimer(timer) { id -> + if (id != -1L) { + insertedCount++ + } + } } } } } return insertedCount } + + private fun isAlarmAlreadyInserted(alarm: Alarm, existingAlarms: List): Boolean { + for (existingAlarm in existingAlarms) { + if (alarm.timeInMinutes == existingAlarm.timeInMinutes && + alarm.days == existingAlarm.days && + alarm.vibrate == existingAlarm.vibrate && + alarm.soundTitle == existingAlarm.soundTitle && + alarm.soundUri == existingAlarm.soundUri && + alarm.label == existingAlarm.label && + alarm.oneShot == existingAlarm.oneShot + ) { + return true + } + } + return false + } + + private fun isTimerAlreadyInserted(timer: Timer, existingTimers: List): Boolean { + for (existingTimer in existingTimers) { + if (timer.seconds == existingTimer.seconds && + timer.vibrate == existingTimer.vibrate && + timer.soundUri == existingTimer.soundUri && + timer.soundTitle == existingTimer.soundTitle && + timer.label == existingTimer.label && + timer.createdAt == existingTimer.createdAt + ) { + return true + } + } + return false + } } From f66488ed2a870289db0b435574eb53f7baa03e00 Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Mon, 29 Apr 2024 10:41:01 +0300 Subject: [PATCH 06/15] fixed timers not importing when list of timers is empty --- .../main/kotlin/org/fossify/clock/helpers/DataImporter.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 cf68ac76..5fc67065 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt @@ -77,7 +77,11 @@ class DataImporter( if (Timer.parseFromJSON(jsonObject) != null) { val timer = Timer.parseFromJSON(jsonObject) as Timer timerHelper.getTimers { existingTimers -> - timer.id = existingTimers.last().id?.plus(1) + timer.id = if (existingTimers.isNotEmpty()) { + existingTimers.last().id?.plus(1) + } else { + 1 + } if (!isTimerAlreadyInserted(timer, existingTimers)) { timerHelper.insertOrUpdateTimer(timer) { id -> if (id != -1L) { From 8c08994bba7d15e85f154dbc13b000e77ae588ea Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Mon, 29 Apr 2024 11:22:55 +0300 Subject: [PATCH 07/15] fixed code formatting --- .../kotlin/org/fossify/clock/activities/SettingsActivity.kt | 3 --- 1 file changed, 3 deletions(-) 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 5b7334f1..dca09753 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -240,7 +240,6 @@ class SettingsActivity : SimpleActivity() { putExtra(Intent.EXTRA_TITLE, file.name) addCategory(Intent.CATEGORY_OPENABLE) - try { exportActivityResultLauncher.launch(file.name) } catch (e: ActivityNotFoundException) { @@ -248,8 +247,6 @@ class SettingsActivity : SimpleActivity() { } catch (e: Exception) { showErrorToast(e) } - - } } } else { From 4c67e2b26c3bd082e7e5d6e8e79e070917726f83 Mon Sep 17 00:00:00 2001 From: ronniedroid Date: Mon, 29 Apr 2024 12:14:09 +0300 Subject: [PATCH 08/15] changed toast messages for importing --- .../fossify/clock/activities/SettingsActivity.kt | 5 +---- .../org/fossify/clock/helpers/DataImporter.kt | 14 +++----------- app/src/main/res/values/strings.xml | 2 -- 3 files changed, 4 insertions(+), 17 deletions(-) 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 dca09753..c8eadc81 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -20,7 +20,6 @@ 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) @@ -323,9 +322,7 @@ class SettingsActivity : SimpleActivity() { DataImporter.ImportResult.IMPORT_OK -> R.string.importing_successful - DataImporter.ImportResult.IMPORT_INCOMPLETE -> R.string.importing_some_entries_failed - DataImporter.ImportResult.ALARMS_IMPORT_FAIL -> CR.string.alarms_import_failed - DataImporter.ImportResult.TIMERS_IMPORT_FAIL -> CR.string.timers_import_failed + DataImporter.ImportResult.IMPORT_INCOMPLETE -> R.string.no_new_entries_for_importing DataImporter.ImportResult.IMPORT_FAIL -> R.string.no_items_found } ) 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 5fc67065..fdb25491 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt @@ -16,8 +16,6 @@ class DataImporter( enum class ImportResult { IMPORT_INCOMPLETE, - ALARMS_IMPORT_FAIL, - TIMERS_IMPORT_FAIL, IMPORT_FAIL, IMPORT_OK } @@ -34,15 +32,9 @@ class DataImporter( 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 + ImportResult.IMPORT_OK + } else if (importedAlarms == 0 && importedTimers == 0) { + ImportResult.IMPORT_INCOMPLETE } else { ImportResult.IMPORT_FAIL } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6fc3b41..5df0ed1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,8 +52,6 @@ Increase volume gradually Import alarms and timers Export alarms and timers - Importing alarms failed - Importing timers failed How can I change lap sorting at the stopwatch tab? From b6e5820045b59bcd9204c38b86478596c02c2875 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Sat, 1 Feb 2025 14:20:59 +0530 Subject: [PATCH 09/15] Use proper style for setting item --- app/src/main/res/layout/activity_settings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 83d5931c..7471758c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -321,7 +321,7 @@ @@ -335,7 +335,7 @@ From f5b0c3197969554c99c7abf3b492173f6cd14e19 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Sun, 2 Feb 2025 20:42:45 +0530 Subject: [PATCH 10/15] Add kotlin serialization --- app/build.gradle.kts | 5 ++++- build.gradle.kts | 1 + gradle/libs.versions.toml | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 11afb601..50d1e5c9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,7 @@ import java.io.FileInputStream plugins { alias(libs.plugins.android) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.kotlinSerialization) alias(libs.plugins.ksp) alias(libs.plugins.detekt) } @@ -75,7 +76,8 @@ android { } compileOptions { - val currentJavaVersionFromLibs = JavaVersion.valueOf(libs.versions.app.build.javaVersion.get()) + val currentJavaVersionFromLibs = + JavaVersion.valueOf(libs.versions.app.build.javaVersion.get()) sourceCompatibility = currentJavaVersionFromLibs targetCompatibility = currentJavaVersionFromLibs } @@ -114,6 +116,7 @@ dependencies { implementation(libs.numberpicker) implementation(libs.autofittextview) implementation(libs.eventbus) + implementation(libs.kotlinx.serialization.json) implementation(libs.bundles.room) ksp(libs.androidx.room.compiler) diff --git a/build.gradle.kts b/build.gradle.kts index b7bc0032..f09a2174 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android).apply(false) alias(libs.plugins.kotlinAndroid).apply(false) + alias(libs.plugins.kotlinSerialization).apply(false) alias(libs.plugins.ksp).apply(false) alias(libs.plugins.detekt).apply(false) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a333cbd..f81d08ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] #jetbrains kotlin = "1.9.22" +kotlinxSerializationJson = "1.6.0" #KSP ksp = "1.9.22-1.0.17" #Detekt @@ -50,6 +51,8 @@ autofittextview = { module = "me.grantland:autofittextview", version.ref = "auto #EventBus eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" } #KotlinX +#Kotlin +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } #NumberPicker numberpicker = { module = "io.github.ShawnLin013:number-picker", version.ref = "numberpicker" } @@ -74,5 +77,6 @@ lifecycle = [ [plugins] android = { id = "com.android.application", version.ref = "gradlePlugins-agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } From 35125775edd861653c91b53ffb0a83fe96c99697 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Mon, 3 Feb 2025 12:02:38 +0530 Subject: [PATCH 11/15] Use kotlin serialization and simplify code - Replaced manual serialization/deserialization with kotlin serialization - Removed unnecessary intent creation - Fixed import/export with SAF on Android 8 & 9 (storage permission was being requested but was never added to the manifest) --- .../clock/activities/SettingsActivity.kt | 216 +++++++----------- .../fossify/clock/dialogs/ExportDataDialog.kt | 55 +++-- .../org/fossify/clock/helpers/Constants.kt | 11 + .../org/fossify/clock/helpers/DataExporter.kt | 51 ----- .../org/fossify/clock/helpers/DataImporter.kt | 121 ---------- .../org/fossify/clock/helpers/ExportHelper.kt | 34 +++ .../org/fossify/clock/helpers/ImportHelper.kt | 109 +++++++++ .../clock/interfaces/JSONConvertible.kt | 5 - .../kotlin/org/fossify/clock/models/Alarm.kt | 52 +---- .../fossify/clock/models/AlarmTimerBackup.kt | 10 + .../kotlin/org/fossify/clock/models/Timer.kt | 53 +---- .../org/fossify/clock/models/TimerState.kt | 5 + 12 files changed, 286 insertions(+), 436 deletions(-) delete mode 100644 app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt delete mode 100644 app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt create mode 100644 app/src/main/kotlin/org/fossify/clock/helpers/ExportHelper.kt create mode 100644 app/src/main/kotlin/org/fossify/clock/helpers/ImportHelper.kt delete mode 100644 app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt create mode 100644 app/src/main/kotlin/org/fossify/clock/models/AlarmTimerBackup.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 8b3ec99a..2fd435ea 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -15,28 +15,27 @@ import org.fossify.clock.extensions.timerDb import org.fossify.clock.helpers.DBHelper import org.fossify.clock.helpers.DEFAULT_MAX_ALARM_REMINDER_SECS import org.fossify.clock.helpers.DEFAULT_MAX_TIMER_REMINDER_SECS -import org.fossify.clock.helpers.DataExporter -import org.fossify.clock.helpers.DataImporter +import org.fossify.clock.helpers.EXPORT_BACKUP_MIME_TYPE +import org.fossify.clock.helpers.ExportHelper +import org.fossify.clock.helpers.IMPORT_BACKUP_MIME_TYPES +import org.fossify.clock.helpers.ImportHelper import org.fossify.clock.helpers.TAB_ALARM import org.fossify.clock.helpers.TAB_CLOCK import org.fossify.clock.helpers.TAB_STOPWATCH import org.fossify.clock.helpers.TAB_TIMER import org.fossify.clock.helpers.TimerHelper -import org.fossify.commons.dialogs.FilePickerDialog +import org.fossify.clock.models.AlarmTimerBackup import org.fossify.commons.dialogs.RadioGroupDialog import org.fossify.commons.extensions.beGoneIf import org.fossify.commons.extensions.beVisibleIf import org.fossify.commons.extensions.formatMinutesToTimeString import org.fossify.commons.extensions.formatSecondsToTimeString import org.fossify.commons.extensions.getCustomizeColorsString -import org.fossify.commons.extensions.getFileOutputStream import org.fossify.commons.extensions.getProperPrimaryColor -import org.fossify.commons.extensions.getTempFile import org.fossify.commons.extensions.isOrWasThankYouInstalled import org.fossify.commons.extensions.launchPurchaseThankYouIntent import org.fossify.commons.extensions.showErrorToast import org.fossify.commons.extensions.showPickSecondsDialog -import org.fossify.commons.extensions.toFileDirItem import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.updateTextColors import org.fossify.commons.extensions.viewBinding @@ -44,20 +43,47 @@ import org.fossify.commons.helpers.ExportResult import org.fossify.commons.helpers.IS_CUSTOMIZING_COLORS import org.fossify.commons.helpers.MINUTE_SECONDS import org.fossify.commons.helpers.NavigationIcon -import org.fossify.commons.helpers.PERMISSION_READ_STORAGE -import org.fossify.commons.helpers.PERMISSION_WRITE_STORAGE import org.fossify.commons.helpers.TAB_LAST_USED import org.fossify.commons.helpers.ensureBackgroundThread -import org.fossify.commons.helpers.isQPlus import org.fossify.commons.helpers.isTiramisuPlus import org.fossify.commons.models.RadioItem -import java.io.FileOutputStream -import java.io.OutputStream +import java.io.IOException import java.util.Locale import kotlin.system.exitProcess class SettingsActivity : SimpleActivity() { private val binding: ActivitySettingsBinding by viewBinding(ActivitySettingsBinding::inflate) + private val exportActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.CreateDocument(EXPORT_BACKUP_MIME_TYPE)) { uri -> + if (uri == null) { + toast(org.fossify.commons.R.string.unknown_error_occurred) + return@registerForActivityResult + } + + ensureBackgroundThread { + try { + exportDataTo(uri) + } catch (e: IOException) { + showErrorToast(e) + } + } + } + + private val importActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) { + toast(org.fossify.commons.R.string.unknown_error_occurred) + return@registerForActivityResult + } + + ensureBackgroundThread { + try { + importData(uri) + } catch (e: Exception) { + showErrorToast(e) + } + } + } override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true @@ -263,144 +289,64 @@ class SettingsActivity : SimpleActivity() { } } - private val exportActivityResultLauncher = - registerForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> - try { - val outputStream = uri?.let { contentResolver.openOutputStream(it) } - if (outputStream != null) { - exportDataTo(outputStream) - } - } catch (e: Exception) { - showErrorToast(e) + private fun exportDataTo(outputUri: Uri) { + val alarms = dbHelper.getAlarms() + val timers = timerDb.getTimers() + if (alarms.isEmpty()) { + toast(org.fossify.commons.R.string.no_entries_for_exporting) + } else { + ExportHelper(this).exportData( + backup = AlarmTimerBackup(alarms, timers), + outputUri = outputUri, + ) { + toast( + when (it) { + ExportResult.EXPORT_OK -> org.fossify.commons.R.string.exporting_successful + else -> org.fossify.commons.R.string.exporting_failed + } + ) } } + } - private val importActivityResultLauncher = - registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + private fun tryExportData() { + ExportDataDialog(this, config.lastDataExportPath) { file -> try { - if (uri != null) { - tryImportDataFromFile(uri) - } + exportActivityResultLauncher.launch(file.name) + } catch (e: ActivityNotFoundException) { + toast( + id = org.fossify.commons.R.string.system_service_disabled, + length = Toast.LENGTH_LONG + ) } catch (e: Exception) { showErrorToast(e) } } - - private fun exportDataTo(outputStream: OutputStream?) { - ensureBackgroundThread { - val alarms = dbHelper.getAlarms() - val timers = timerDb.getTimers() - if (alarms.isEmpty()) { - toast(org.fossify.commons.R.string.no_entries_for_exporting) - } else { - DataExporter.exportData(alarms, timers, outputStream) { - toast( - when (it) { - ExportResult.EXPORT_OK -> org.fossify.commons.R.string.exporting_successful - else -> org.fossify.commons.R.string.exporting_failed - } - ) - } - } - } - } - - private fun tryExportData() { - if (isQPlus()) { - ExportDataDialog(this, config.lastDataExportPath, true) { file -> - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - putExtra(Intent.EXTRA_TITLE, file.name) - addCategory(Intent.CATEGORY_OPENABLE) - - try { - exportActivityResultLauncher.launch(file.name) - } catch (e: ActivityNotFoundException) { - toast( - org.fossify.commons.R.string.system_service_disabled, - Toast.LENGTH_LONG - ) - } catch (e: Exception) { - showErrorToast(e) - } - } - } - } else { - handlePermission(PERMISSION_WRITE_STORAGE) { isAllowed -> - if (isAllowed) { - ExportDataDialog(this, config.lastDataExportPath, false) { file -> - getFileOutputStream(file.toFileDirItem(this), true) { out -> - exportDataTo(out) - } - } - } - } - } } 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(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) - } catch (e: Exception) { - showErrorToast(e) - } - } - } else { - handlePermission(PERMISSION_READ_STORAGE) { isAllowed -> - if (isAllowed) { - pickFileToImportData() - } - } + try { + importActivityResultLauncher.launch(IMPORT_BACKUP_MIME_TYPES.toTypedArray()) + } catch (e: ActivityNotFoundException) { + toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) } } - 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(org.fossify.commons.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) - } + private fun importData(uri: Uri) { + val result = ImportHelper( + context = this, + dbHelper = DBHelper.dbInstance!!, + timerHelper = TimerHelper(this) + ).importData(uri) + + toast( + when (result) { + ImportHelper.ImportResult.IMPORT_OK -> org.fossify.commons.R.string.importing_successful + ImportHelper.ImportResult.IMPORT_INCOMPLETE -> org.fossify.commons.R.string.no_new_entries_for_importing + ImportHelper.ImportResult.IMPORT_FAIL -> org.fossify.commons.R.string.no_items_found } - - else -> toast(org.fossify.commons.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 -> org.fossify.commons.R.string.importing_successful - DataImporter.ImportResult.IMPORT_INCOMPLETE -> org.fossify.commons.R.string.no_new_entries_for_importing - DataImporter.ImportResult.IMPORT_FAIL -> org.fossify.commons.R.string.no_items_found - } - ) - } + ) } } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt index 6ab79fb9..920fbe5e 100644 --- a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt @@ -1,48 +1,57 @@ package org.fossify.clock.dialogs +import android.annotation.SuppressLint import androidx.appcompat.app.AlertDialog -import org.fossify.commons.activities.BaseSimpleActivity -import org.fossify.commons.dialogs.FilePickerDialog -import org.fossify.commons.extensions.* -import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.clock.R import org.fossify.clock.databinding.DialogExportDataBinding import org.fossify.clock.extensions.config import org.fossify.clock.helpers.DATA_EXPORT_EXTENSION +import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.extensions.beGone +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.getCurrentFormattedDateTime +import org.fossify.commons.extensions.getParentPath +import org.fossify.commons.extensions.humanizePath +import org.fossify.commons.extensions.internalStoragePath +import org.fossify.commons.extensions.isAValidFilename +import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.extensions.showKeyboard +import org.fossify.commons.extensions.toast +import org.fossify.commons.extensions.value +import org.fossify.commons.helpers.ensureBackgroundThread import java.io.File +@SuppressLint("SetTextI18n") class ExportDataDialog( - val activity: BaseSimpleActivity, - val path: String, - val hidePath: Boolean, - callback: (file: File) -> Unit, + private val activity: BaseSimpleActivity, + private val path: String, + private val callback: (file: File) -> Unit, ) { + + companion object { + private const val EXPORT_FILE_NAME = "alarms_and_timers" + } + private var realPath = path.ifEmpty { activity.internalStoragePath } private val config = activity.config init { val view = DialogExportDataBinding.inflate(activity.layoutInflater, null, false).apply { exportDataFolder.text = activity.humanizePath(realPath) - exportDataFilename.setText("${activity.getString(R.string.settings_export_data)}_${activity.getCurrentFormattedDateTime()}") - - if (hidePath) { - exportDataFolderLabel.beGone() - exportDataFolder.beGone() - } else { - exportDataFolder.setOnClickListener { - FilePickerDialog(activity, realPath, false, showFAB = true) { - exportDataFolder.text = activity.humanizePath(it) - realPath = it - } - } - } + exportDataFilename.setText("${EXPORT_FILE_NAME}_${activity.getCurrentFormattedDateTime()}") + exportDataFolderLabel.beGone() + exportDataFolder.beGone() } activity.getAlertDialogBuilder() .setPositiveButton(org.fossify.commons.R.string.ok, null) .setNegativeButton(org.fossify.commons.R.string.cancel, null) .apply { - activity.setupDialogStuff(view.root, this, R.string.settings_export_data) { alertDialog -> + activity.setupDialogStuff( + view = view.root, + dialog = this, + titleId = R.string.settings_export_data + ) { alertDialog -> alertDialog.showKeyboard(view.exportDataFilename) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { val filename = view.exportDataFilename.value @@ -50,7 +59,7 @@ class ExportDataDialog( filename.isEmpty() -> activity.toast(org.fossify.commons.R.string.empty_name) filename.isAValidFilename() -> { val file = File(realPath, "$filename$DATA_EXPORT_EXTENSION") - if (!hidePath && file.exists()) { + if (file.exists()) { activity.toast(org.fossify.commons.R.string.name_taken) return@setOnClickListener } diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt index 48816295..6473f2ec 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt @@ -91,6 +91,17 @@ val DAY_BIT_MAP = mapOf( Calendar.SATURDAY to SATURDAY_BIT, ) +// Import/export +const val EXPORT_BACKUP_MIME_TYPE = "application/json" +val IMPORT_BACKUP_MIME_TYPES = buildList { + add("application/json") + if (!isPiePlus()) { + // Workaround for https://github.com/FossifyOrg/Messages/issues/88 + add("application/octet-stream") + } +} + + fun getDefaultTimeZoneTitle(id: Int) = getAllTimeZones().firstOrNull { it.id == id }?.title ?: "" fun getPassedSeconds(): Int { diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt deleted file mode 100644 index 43f7c626..00000000 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataExporter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.fossify.clock.helpers - -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: List, - timers: List, - outputStream: OutputStream?, - callback: (result: ExportResult) -> Unit, - ) { - if (outputStream == null) { - callback.invoke(ExportResult.EXPORT_FAIL) - return - } - - val alarmsJsonArray = toJsonArray(alarms) - val timersJsonArray = toJsonArray(timers) - - val jsonObject = JSONObject().apply { - put("alarms", alarmsJsonArray) - put("timers", timersJsonArray) - } - - try { - outputStream.bufferedWriter().use { out -> - out.write(jsonObject.toString()) - } - callback.invoke(ExportResult.EXPORT_OK) - } catch (e: Exception) { - callback.invoke(ExportResult.EXPORT_FAIL) - } - } - - private fun toJsonArray(list: List): JSONArray { - return if (list.isEmpty()) { - JSONArray() - } else { - JSONArray().apply { - list.forEach { item -> - put(JSONObject(item.toJSON())) - } - } - } - } -} diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt b/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt deleted file mode 100644 index fdb25491..00000000 --- a/app/src/main/kotlin/org/fossify/clock/helpers/DataImporter.kt +++ /dev/null @@ -1,121 +0,0 @@ -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_INCOMPLETE, - 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 jsonObject = JSONObject(jsonString) - val alarmsFromJson = jsonObject.getJSONArray("alarms") - val timersFromJson = jsonObject.getJSONArray("timers") - - val importedAlarms = insertAlarmsFromJSON(alarmsFromJson) - val importedTimers = insertTimersFromJSON(timersFromJson) - - if (importedAlarms > 0 || importedTimers > 0) { - ImportResult.IMPORT_OK - } else if (importedAlarms == 0 && importedTimers == 0) { - ImportResult.IMPORT_INCOMPLETE - } else { - ImportResult.IMPORT_FAIL - } - } catch (e: Exception) { - activity.showErrorToast(e) - ImportResult.IMPORT_FAIL - } - } - - - private fun insertAlarmsFromJSON(jsonArray: JSONArray): Int { - val existingAlarms = dbHelper.getAlarms() - var insertedCount = 0 - for (i in 0 until jsonArray.length()) { - val jsonObject = jsonArray.getJSONObject(i) - if (Alarm.parseFromJSON(jsonObject) != null) { - val alarm = Alarm.parseFromJSON(jsonObject) as Alarm - if (!isAlarmAlreadyInserted(alarm, existingAlarms)) { - if (dbHelper.insertAlarm(alarm) != -1) { - insertedCount++ - } - } - } - } - return insertedCount - } - - 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.getTimers { existingTimers -> - timer.id = if (existingTimers.isNotEmpty()) { - existingTimers.last().id?.plus(1) - } else { - 1 - } - if (!isTimerAlreadyInserted(timer, existingTimers)) { - timerHelper.insertOrUpdateTimer(timer) { id -> - if (id != -1L) { - insertedCount++ - } - } - } - } - } - } - return insertedCount - } - - private fun isAlarmAlreadyInserted(alarm: Alarm, existingAlarms: List): Boolean { - for (existingAlarm in existingAlarms) { - if (alarm.timeInMinutes == existingAlarm.timeInMinutes && - alarm.days == existingAlarm.days && - alarm.vibrate == existingAlarm.vibrate && - alarm.soundTitle == existingAlarm.soundTitle && - alarm.soundUri == existingAlarm.soundUri && - alarm.label == existingAlarm.label && - alarm.oneShot == existingAlarm.oneShot - ) { - return true - } - } - return false - } - - private fun isTimerAlreadyInserted(timer: Timer, existingTimers: List): Boolean { - for (existingTimer in existingTimers) { - if (timer.seconds == existingTimer.seconds && - timer.vibrate == existingTimer.vibrate && - timer.soundUri == existingTimer.soundUri && - timer.soundTitle == existingTimer.soundTitle && - timer.label == existingTimer.label && - timer.createdAt == existingTimer.createdAt - ) { - return true - } - } - return false - } -} - diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/ExportHelper.kt b/app/src/main/kotlin/org/fossify/clock/helpers/ExportHelper.kt new file mode 100644 index 00000000..5e5513a7 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/helpers/ExportHelper.kt @@ -0,0 +1,34 @@ +package org.fossify.clock.helpers + +import android.content.Context +import android.net.Uri +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream +import org.fossify.clock.models.AlarmTimerBackup +import org.fossify.commons.helpers.ExportResult + +class ExportHelper(private val context: Context) { + + @OptIn(ExperimentalSerializationApi::class) + fun exportData( + backup: AlarmTimerBackup, + outputUri: Uri?, + callback: (result: ExportResult) -> Unit, + ) { + if (outputUri == null) { + callback.invoke(ExportResult.EXPORT_FAIL) + return + } + + try { + val json = Json { encodeDefaults = true } + context.contentResolver.openOutputStream(outputUri)?.use { out -> + json.encodeToStream(backup, out) + callback.invoke(ExportResult.EXPORT_OK) + } ?: throw NullPointerException("Output stream is null") + } catch (e: Exception) { + callback.invoke(ExportResult.EXPORT_FAIL) + } + } +} diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/ImportHelper.kt b/app/src/main/kotlin/org/fossify/clock/helpers/ImportHelper.kt new file mode 100644 index 00000000..bcb524d7 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/helpers/ImportHelper.kt @@ -0,0 +1,109 @@ +package org.fossify.clock.helpers + +import android.content.Context +import android.net.Uri +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import org.fossify.clock.models.Alarm +import org.fossify.clock.models.AlarmTimerBackup +import org.fossify.clock.models.Timer +import org.fossify.commons.extensions.showErrorToast + +class ImportHelper( + private val context: Context, + private val dbHelper: DBHelper, + private val timerHelper: TimerHelper, +) { + + enum class ImportResult { + IMPORT_INCOMPLETE, + IMPORT_FAIL, + IMPORT_OK + } + + @OptIn(ExperimentalSerializationApi::class) + fun importData(uri: Uri): ImportResult { + return try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val backup = Json.decodeFromStream(inputStream) + val importedAlarms = insertAlarms(backup.alarms) + val importedTimers = insertTimers(backup.timers) + when { + importedAlarms > 0 || importedTimers > 0 -> ImportResult.IMPORT_OK + importedAlarms == 0 && importedTimers == 0 -> ImportResult.IMPORT_INCOMPLETE + else -> ImportResult.IMPORT_FAIL + } + } ?: ImportResult.IMPORT_FAIL + } catch (e: Exception) { + context.showErrorToast(e) + ImportResult.IMPORT_FAIL + } + } + + private fun insertAlarms(alarms: List): Int { + val existingAlarms = dbHelper.getAlarms() + var insertedCount = 0 + alarms.forEach { alarm -> + if (!isAlarmAlreadyInserted(alarm, existingAlarms)) { + if (dbHelper.insertAlarm(alarm) != -1) { + insertedCount++ + } + } + } + return insertedCount + } + + private fun insertTimers(timers: List): Int { + var insertedCount = 0 + timers.forEach { timer -> + timerHelper.getTimers { existingTimers -> + timer.id = if (existingTimers.isNotEmpty()) { + existingTimers.last().id?.plus(1) + } else { + 1 + } + if (!isTimerAlreadyInserted(timer, existingTimers)) { + timerHelper.insertOrUpdateTimer(timer) { id -> + if (id != -1L) { + insertedCount++ + } + } + } + } + } + return insertedCount + } + + private fun isAlarmAlreadyInserted(alarm: Alarm, existingAlarms: List): Boolean { + for (existingAlarm in existingAlarms) { + if (alarm.timeInMinutes == existingAlarm.timeInMinutes && + alarm.days == existingAlarm.days && + alarm.vibrate == existingAlarm.vibrate && + alarm.soundTitle == existingAlarm.soundTitle && + alarm.soundUri == existingAlarm.soundUri && + alarm.label == existingAlarm.label && + alarm.oneShot == existingAlarm.oneShot + ) { + return true + } + } + return false + } + + private fun isTimerAlreadyInserted(timer: Timer, existingTimers: List): Boolean { + for (existingTimer in existingTimers) { + if (timer.seconds == existingTimer.seconds && + timer.vibrate == existingTimer.vibrate && + timer.soundUri == existingTimer.soundUri && + timer.soundTitle == existingTimer.soundTitle && + timer.label == existingTimer.label && + timer.createdAt == existingTimer.createdAt + ) { + return true + } + } + return false + } +} + diff --git a/app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt b/app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt deleted file mode 100644 index b0977b11..00000000 --- a/app/src/main/kotlin/org/fossify/clock/interfaces/JSONConvertible.kt +++ /dev/null @@ -1,5 +0,0 @@ -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 674986c5..9d068411 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt @@ -1,10 +1,9 @@ package org.fossify.clock.models import androidx.annotation.Keep -import org.fossify.clock.interfaces.JSONConvertible -import org.json.JSONObject @Keep +@kotlinx.serialization.Serializable data class Alarm( var id: Int, var timeInMinutes: Int, @@ -15,54 +14,7 @@ data class Alarm( var soundUri: String, var label: String, var oneShot: Boolean = false, -) : JSONConvertible { - override fun toJSON(): String { - val jsonObject = JSONObject() - jsonObject.put("id", id) - jsonObject.put("timeInMinutes", timeInMinutes) - jsonObject.put("days", days) - jsonObject.put("vibrate", vibrate) - jsonObject.put("soundTitle", soundTitle) - jsonObject.put("soundUri", soundUri) - jsonObject.put("label", label) - 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 data class ObfuscatedAlarm( diff --git a/app/src/main/kotlin/org/fossify/clock/models/AlarmTimerBackup.kt b/app/src/main/kotlin/org/fossify/clock/models/AlarmTimerBackup.kt new file mode 100644 index 00000000..c7deac16 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/models/AlarmTimerBackup.kt @@ -0,0 +1,10 @@ +package org.fossify.clock.models + +import androidx.annotation.Keep + +@Keep +@kotlinx.serialization.Serializable +data class AlarmTimerBackup( + val alarms: List, + val timers: List, +) 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 523af0d8..7899a787 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/Timer.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/Timer.kt @@ -3,11 +3,10 @@ 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") @Keep +@kotlinx.serialization.Serializable data class Timer( @PrimaryKey(autoGenerate = true) var id: Int?, var seconds: Int, @@ -19,55 +18,7 @@ data class Timer( var createdAt: Long, var channelId: String? = null, var oneShot: Boolean = false, -) : JSONConvertible { - override fun toJSON(): String { - val jsonObject = JSONObject() - jsonObject.put("id", id) - 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("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 data class ObfuscatedTimer( diff --git a/app/src/main/kotlin/org/fossify/clock/models/TimerState.kt b/app/src/main/kotlin/org/fossify/clock/models/TimerState.kt index 1737e9d4..26b70337 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/TimerState.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/TimerState.kt @@ -3,16 +3,21 @@ package org.fossify.clock.models import androidx.annotation.Keep @Keep +@kotlinx.serialization.Serializable sealed class TimerState { @Keep + @kotlinx.serialization.Serializable object Idle : TimerState() @Keep + @kotlinx.serialization.Serializable data class Running(val duration: Long, val tick: Long) : TimerState() @Keep + @kotlinx.serialization.Serializable data class Paused(val duration: Long, val tick: Long) : TimerState() @Keep + @kotlinx.serialization.Serializable object Finished : TimerState() } From c4f429ad90f244a591d0f0e552e5ba2b979cae59 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Mon, 3 Feb 2025 16:25:28 +0530 Subject: [PATCH 12/15] Fix lint errors --- .../org/fossify/clock/activities/SettingsActivity.kt | 12 +++++++++--- .../org/fossify/clock/dialogs/ExportDataDialog.kt | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) 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 2fd435ea..ed4eb03b 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -26,7 +26,9 @@ import org.fossify.clock.helpers.TAB_TIMER import org.fossify.clock.helpers.TimerHelper import org.fossify.clock.models.AlarmTimerBackup import org.fossify.commons.dialogs.RadioGroupDialog +import org.fossify.commons.extensions.beGone import org.fossify.commons.extensions.beGoneIf +import org.fossify.commons.extensions.beVisible import org.fossify.commons.extensions.beVisibleIf import org.fossify.commons.extensions.formatMinutesToTimeString import org.fossify.commons.extensions.formatSecondsToTimeString @@ -156,9 +158,13 @@ class SettingsActivity : SimpleActivity() { private fun setupLanguage() { binding.settingsLanguage.text = Locale.getDefault().displayLanguage - binding.settingsLanguageHolder.beVisibleIf(isTiramisuPlus()) - binding.settingsLanguageHolder.setOnClickListener { - launchChangeAppLanguageIntent() + if (isTiramisuPlus()) { + binding.settingsLanguageHolder.beVisible() + binding.settingsLanguageHolder.setOnClickListener { + launchChangeAppLanguageIntent() + } + } else { + binding.settingsLanguageHolder.beGone() } } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt index 920fbe5e..144facb9 100644 --- a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt @@ -24,7 +24,7 @@ import java.io.File @SuppressLint("SetTextI18n") class ExportDataDialog( private val activity: BaseSimpleActivity, - private val path: String, + path: String, private val callback: (file: File) -> Unit, ) { @@ -32,7 +32,7 @@ class ExportDataDialog( private const val EXPORT_FILE_NAME = "alarms_and_timers" } - private var realPath = path.ifEmpty { activity.internalStoragePath } + private val realPath = path.ifEmpty { activity.internalStoragePath } private val config = activity.config init { From 5a5009245c0f428721eb518f463373fbeea4d1b3 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Mon, 3 Feb 2025 16:44:24 +0530 Subject: [PATCH 13/15] Remove unnecessary toast User pressing back is not an error. --- .../org/fossify/clock/activities/SettingsActivity.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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 ed4eb03b..9f2365f5 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -57,11 +57,7 @@ class SettingsActivity : SimpleActivity() { private val binding: ActivitySettingsBinding by viewBinding(ActivitySettingsBinding::inflate) private val exportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument(EXPORT_BACKUP_MIME_TYPE)) { uri -> - if (uri == null) { - toast(org.fossify.commons.R.string.unknown_error_occurred) - return@registerForActivityResult - } - + if (uri == null) return@registerForActivityResult ensureBackgroundThread { try { exportDataTo(uri) @@ -73,11 +69,7 @@ class SettingsActivity : SimpleActivity() { private val importActivityResultLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - if (uri == null) { - toast(org.fossify.commons.R.string.unknown_error_occurred) - return@registerForActivityResult - } - + if (uri == null) return@registerForActivityResult ensureBackgroundThread { try { importData(uri) From 6feb093ca66a0797384869e345e423dd082a4c23 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Tue, 4 Feb 2025 14:54:01 +0530 Subject: [PATCH 14/15] Allow exporting timers alarms or timers Timers alone couldn't be exported before. --- .../kotlin/org/fossify/clock/activities/SettingsActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9f2365f5..d81eb8ec 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -290,7 +290,7 @@ class SettingsActivity : SimpleActivity() { private fun exportDataTo(outputUri: Uri) { val alarms = dbHelper.getAlarms() val timers = timerDb.getTimers() - if (alarms.isEmpty()) { + if (alarms.isEmpty() && timers.isEmpty()) { toast(org.fossify.commons.R.string.no_entries_for_exporting) } else { ExportHelper(this).exportData( From 4fef367a1f95a684a438a6ec92a9c9602a275b0c Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Tue, 4 Feb 2025 15:01:02 +0530 Subject: [PATCH 15/15] Remove unnecessary widgets from export dialog --- .../fossify/clock/dialogs/ExportDataDialog.kt | 5 -- .../main/res/layout/dialog_export_data.xml | 60 ++++++------------- 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt index 144facb9..a61d401e 100644 --- a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt @@ -7,11 +7,9 @@ import org.fossify.clock.databinding.DialogExportDataBinding import org.fossify.clock.extensions.config import org.fossify.clock.helpers.DATA_EXPORT_EXTENSION import org.fossify.commons.activities.BaseSimpleActivity -import org.fossify.commons.extensions.beGone import org.fossify.commons.extensions.getAlertDialogBuilder import org.fossify.commons.extensions.getCurrentFormattedDateTime import org.fossify.commons.extensions.getParentPath -import org.fossify.commons.extensions.humanizePath import org.fossify.commons.extensions.internalStoragePath import org.fossify.commons.extensions.isAValidFilename import org.fossify.commons.extensions.setupDialogStuff @@ -37,10 +35,7 @@ class ExportDataDialog( init { val view = DialogExportDataBinding.inflate(activity.layoutInflater, null, false).apply { - exportDataFolder.text = activity.humanizePath(realPath) exportDataFilename.setText("${EXPORT_FILE_NAME}_${activity.getCurrentFormattedDateTime()}") - exportDataFolderLabel.beGone() - exportDataFolder.beGone() } activity.getAlertDialogBuilder() diff --git a/app/src/main/res/layout/dialog_export_data.xml b/app/src/main/res/layout/dialog_export_data.xml index f85c98c0..392ef38d 100644 --- a/app/src/main/res/layout/dialog_export_data.xml +++ b/app/src/main/res/layout/dialog_export_data.xml @@ -1,51 +1,27 @@ - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingLeft="@dimen/activity_margin" + android:paddingTop="@dimen/activity_margin" + android:paddingRight="@dimen/activity_margin"> - + android:hint="@string/filename_without_json"> - - - - - - - + android:layout_marginBottom="@dimen/activity_margin" + android:singleLine="true" + android:textCursorDrawable="@null" + android:textSize="@dimen/normal_text_size" /> - - - + + \ No newline at end of file