Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 5 additions & 6 deletions app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
156 changes: 148 additions & 8 deletions app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt
Original file line number Diff line number Diff line change
@@ -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)
}

Expand All @@ -51,6 +110,8 @@ class SettingsActivity : SimpleActivity() {
setupTimerMaxReminder()
setupIncreaseVolumeGradually()
setupCustomizeWidgetColors()
setupExportData()
setupImportData()
updateTextColors(binding.settingsHolder)

arrayOf(
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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() {
Expand All @@ -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
}
)
}
}
76 changes: 76 additions & 0 deletions app/src/main/kotlin/org/fossify/clock/dialogs/ExportDataDialog.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
}

4 changes: 4 additions & 0 deletions app/src/main/kotlin/org/fossify/clock/helpers/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
13 changes: 13 additions & 0 deletions app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ":"
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading