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/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt index 4379230d..ad5713ed 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt @@ -13,10 +13,7 @@ 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.* @@ -171,8 +168,10 @@ 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) + } } } 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 8d531872..d81eb8ec 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt @@ -1,36 +1,95 @@ 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 import org.fossify.clock.R import org.fossify.clock.databinding.ActivitySettingsBinding +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.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.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.clock.models.AlarmTimerBackup import org.fossify.commons.dialogs.RadioGroupDialog -import org.fossify.commons.extensions.* +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 +import org.fossify.commons.extensions.getCustomizeColorsString +import org.fossify.commons.extensions.getProperPrimaryColor +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.toast +import org.fossify.commons.extensions.updateTextColors +import org.fossify.commons.extensions.viewBinding +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.TAB_LAST_USED import org.fossify.commons.helpers.NavigationIcon +import org.fossify.commons.helpers.TAB_LAST_USED +import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.commons.helpers.isTiramisuPlus import org.fossify.commons.models.RadioItem +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) return@registerForActivityResult + ensureBackgroundThread { + try { + exportDataTo(uri) + } catch (e: IOException) { + showErrorToast(e) + } + } + } + + private val importActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) return@registerForActivityResult + ensureBackgroundThread { + try { + importData(uri) + } catch (e: Exception) { + showErrorToast(e) + } + } + } override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true super.onCreate(savedInstanceState) setContentView(binding.root) - updateMaterialActivityViews(binding.settingsCoordinator, binding.settingsHolder, useTransparentNavigation = true, useTopSearchMenu = false) + updateMaterialActivityViews( + mainCoordinatorLayout = binding.settingsCoordinator, + nestedView = binding.settingsHolder, + useTransparentNavigation = true, + useTopSearchMenu = false + ) setupMaterialScrollListener(binding.settingsNestedScrollview, binding.settingsToolbar) } @@ -51,6 +110,8 @@ class SettingsActivity : SimpleActivity() { setupTimerMaxReminder() setupIncreaseVolumeGradually() setupCustomizeWidgetColors() + setupExportData() + setupImportData() updateTextColors(binding.settingsHolder) arrayOf( @@ -89,9 +150,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() } } @@ -192,11 +257,13 @@ class SettingsActivity : SimpleActivity() { } private fun updateAlarmMaxReminderText() { - binding.settingsAlarmMaxReminder.text = formatSecondsToTimeString(config.alarmMaxReminderSecs) + binding.settingsAlarmMaxReminder.text = + formatSecondsToTimeString(config.alarmMaxReminderSecs) } private fun updateTimerMaxReminderText() { - binding.settingsTimerMaxReminder.text = formatSecondsToTimeString(config.timerMaxReminderSecs) + binding.settingsTimerMaxReminder.text = + formatSecondsToTimeString(config.timerMaxReminderSecs) } private fun setupCustomizeWidgetColors() { @@ -207,4 +274,77 @@ class SettingsActivity : SimpleActivity() { } } } + + private fun setupExportData() { + binding.settingsExportDataHolder.setOnClickListener { + tryExportData() + } + } + + private fun setupImportData() { + binding.settingsImportDataHolder.setOnClickListener { + tryImportData() + } + } + + private fun exportDataTo(outputUri: Uri) { + val alarms = dbHelper.getAlarms() + val timers = timerDb.getTimers() + if (alarms.isEmpty() && timers.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 fun tryExportData() { + ExportDataDialog(this, config.lastDataExportPath) { file -> + try { + 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 tryImportData() { + 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 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 + } + ) + } } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt new file mode 100644 index 00000000..a61d401e --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt @@ -0,0 +1,76 @@ +package org.fossify.clock.dialogs + +import android.annotation.SuppressLint +import androidx.appcompat.app.AlertDialog +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.getAlertDialogBuilder +import org.fossify.commons.extensions.getCurrentFormattedDateTime +import org.fossify.commons.extensions.getParentPath +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( + private val activity: BaseSimpleActivity, + path: String, + private val callback: (file: File) -> Unit, +) { + + companion object { + private const val EXPORT_FILE_NAME = "alarms_and_timers" + } + + private val realPath = path.ifEmpty { activity.internalStoragePath } + private val config = activity.config + + init { + val view = DialogExportDataBinding.inflate(activity.layoutInflater, null, false).apply { + exportDataFilename.setText("${EXPORT_FILE_NAME}_${activity.getCurrentFormattedDateTime()}") + } + + activity.getAlertDialogBuilder() + .setPositiveButton(org.fossify.commons.R.string.ok, null) + .setNegativeButton(org.fossify.commons.R.string.cancel, null) + .apply { + 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 + when { + filename.isEmpty() -> activity.toast(org.fossify.commons.R.string.empty_name) + filename.isAValidFilename() -> { + val file = File(realPath, "$filename$DATA_EXPORT_EXTENSION") + if (file.exists()) { + activity.toast(org.fossify.commons.R.string.name_taken) + return@setOnClickListener + } + + ensureBackgroundThread { + config.lastDataExportPath = 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/Config.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt index 2dbf8cdc..f069d578 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 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 a78eba9e..6473f2ec 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 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 = ":" @@ -89,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/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/models/Alarm.kt b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt index fc939900..9d068411 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt @@ -3,6 +3,7 @@ package org.fossify.clock.models import androidx.annotation.Keep @Keep +@kotlinx.serialization.Serializable data class Alarm( var id: Int, var timeInMinutes: Int, 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 f20dbe18..7899a787 100644 --- a/app/src/main/kotlin/org/fossify/clock/models/Timer.kt +++ b/app/src/main/kotlin/org/fossify/clock/models/Timer.kt @@ -6,6 +6,7 @@ import androidx.room.PrimaryKey @Entity(tableName = "timers") @Keep +@kotlinx.serialization.Serializable data class Timer( @PrimaryKey(autoGenerate = true) var id: Int?, var seconds: Int, 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() } diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index fc662aae..7471758c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -306,6 +306,47 @@ tools:text="1 minute" /> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_export_data.xml b/app/src/main/res/layout/dialog_export_data.xml new file mode 100644 index 00000000..392ef38d --- /dev/null +++ b/app/src/main/res/layout/dialog_export_data.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bcde8ab..5df0ed1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,8 @@ Timer tab Show seconds Increase volume gradually + Import alarms and timers + Export alarms and timers How can I change lap sorting at the stopwatch tab? 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" }