From 29d747d8a10cd1768a7e798b651e5fb01937d15c Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:33:11 +0100 Subject: [PATCH 01/32] Added alerts section Signed-off-by: Arnau Mora --- app/src/main/res/layout/title_color.xml | 85 ++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/title_color.xml b/app/src/main/res/layout/title_color.xml index 0042ee67..8611f9f9 100644 --- a/app/src/main/res/layout/title_color.xml +++ b/app/src/main/res/layout/title_color.xml @@ -2,20 +2,24 @@ + - + + + android:layout_height="match_parent" + android:orientation="vertical"> + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" /> + android:text="@{model.url}" + android:textIsSelectable="true" /> @@ -60,11 +64,76 @@ android:layout_gravity="center" android:layout_marginLeft="16dp" app:color="@{model.color}" - tools:ignore="RtlHardcoded"/> + tools:ignore="RtlHardcoded" /> + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From c5910d16b7e2eb158af3e07c3db92a600284866a Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:33:32 +0100 Subject: [PATCH 02/32] Added allowed reminders fetching Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index 5ec3b2c7..aeb9ed66 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -11,10 +11,12 @@ import android.content.ContentValues import android.os.RemoteException import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Events +import android.util.Log import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidCalendarFactory import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.icsdroid.Constants class LocalCalendar private constructor( account: Account, @@ -30,6 +32,7 @@ class LocalCalendar private constructor( const val COLUMN_LAST_MODIFIED = Calendars.CAL_SYNC4 const val COLUMN_LAST_SYNC = Calendars.CAL_SYNC5 const val COLUMN_ERROR_MESSAGE = Calendars.CAL_SYNC6 + const val COLUMN_ALLOWED_REMINDERS = Calendars.ALLOWED_REMINDERS fun findById(account: Account, provider: ContentProviderClient, id: Long) = findByID(account, provider, Factory, id) @@ -46,6 +49,8 @@ class LocalCalendar private constructor( var lastSync = 0L // time of last sync (0 if none) var errorMessage: String? = null // error message (HTTP status or exception name) of last sync (or null) + var allowedReminders: List = listOf() + override fun populate(info: ContentValues) { super.populate(info) @@ -56,6 +61,12 @@ class LocalCalendar private constructor( info.getAsLong(COLUMN_LAST_SYNC)?.let { lastSync = it } errorMessage = info.getAsString(COLUMN_ERROR_MESSAGE) + + info.getAsString(COLUMN_ALLOWED_REMINDERS) + ?.split(',') + ?.mapNotNull { it.toIntOrNull() } + ?.let { allowedReminders = it } + Log.i(Constants.TAG, "Allowed reminders: $allowedReminders") } fun updateStatusSuccess(eTag: String?, lastModified: Long) { @@ -63,11 +74,12 @@ class LocalCalendar private constructor( this.lastModified = lastModified lastSync = System.currentTimeMillis() - val values = ContentValues(4) + val values = ContentValues(5) values.put(COLUMN_ETAG, eTag) values.put(COLUMN_LAST_MODIFIED, lastModified) values.put(COLUMN_LAST_SYNC, lastSync) values.putNull(COLUMN_ERROR_MESSAGE) + values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) update(values) } @@ -85,11 +97,12 @@ class LocalCalendar private constructor( lastSync = System.currentTimeMillis() errorMessage = message - val values = ContentValues(4) + val values = ContentValues(5) values.putNull(COLUMN_ETAG) values.putNull(COLUMN_LAST_MODIFIED) values.put(COLUMN_LAST_SYNC, lastSync) values.put(COLUMN_ERROR_MESSAGE, message) + values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) update(values) } From c9129157ea9ea53407da2d657fcd0f30bce07c63 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:33:42 +0100 Subject: [PATCH 03/32] Added `LifecycleViewHolder` Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/ui/LifecycleViewHolder.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt b/app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt new file mode 100644 index 00000000..37967d59 --- /dev/null +++ b/app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt @@ -0,0 +1,16 @@ +package at.bitfire.icsdroid.ui + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +/** + * A [RecyclerView.ViewHolder] that is aware of its [LifecycleOwner]. Also adapts directly the given [ViewBinding]. + * @since 20221202 + * @param binding The [ViewBinding] to give to the ViewHolder. + */ +abstract class LifecycleViewHolder (binding: B): RecyclerView.ViewHolder(binding.root) { + val lifecycleOwner by lazy { + binding.root.context as? LifecycleOwner + } +} From 31b4e1ea9108378d80b02fa365af3359b20327c1 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:33:55 +0100 Subject: [PATCH 04/32] Added row for custom alerts Signed-off-by: Arnau Mora --- app/src/main/res/layout/alert_row.xml | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 app/src/main/res/layout/alert_row.xml diff --git a/app/src/main/res/layout/alert_row.xml b/app/src/main/res/layout/alert_row.xml new file mode 100644 index 00000000..7331daf8 --- /dev/null +++ b/app/src/main/res/layout/alert_row.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + From 1bf2b62769ef8554668fe198b3a1ebe588e03343 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:34:14 +0100 Subject: [PATCH 05/32] Added alerts section displaying Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 180 ++++++++++++++++-- 1 file changed, 162 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index 012cff69..d8136f0f 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -4,45 +4,64 @@ package at.bitfire.icsdroid.ui -import android.content.Intent +import android.content.Context import android.os.Bundle +import android.provider.CalendarContract +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.ArrayAdapter +import androidx.annotation.IntDef +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import at.bitfire.icsdroid.Constants +import at.bitfire.icsdroid.R +import at.bitfire.icsdroid.databinding.AlertRowBinding import at.bitfire.icsdroid.databinding.TitleColorBinding -import at.bitfire.icsdroid.db.LocalCalendar -class TitleColorFragment: Fragment() { +class TitleColorFragment : Fragment() { private val model by activityViewModels() + private val colorPickerContract = registerForActivityResult(ColorPickerActivity.Contract()) { color -> + model.color.postValue(color) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, inState: Bundle?): View { val binding = TitleColorBinding.inflate(inflater, container, false) binding.lifecycleOwner = this binding.model = model - binding.color.setOnClickListener { - val intent = Intent(requireActivity(), ColorPickerActivity::class.java) - model.color.value?.let { - intent.putExtra(ColorPickerActivity.EXTRA_COLOR, it) - } - startActivityForResult(intent, 0) - } - return binding.root - } + // Listener for launching the color picker + binding.color.setOnClickListener { colorPickerContract.launch(model.color.value) } - override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { - result?.let { - model.color.value = it.getIntExtra(ColorPickerActivity.EXTRA_COLOR, LocalCalendar.DEFAULT_COLOR) + // Initialize the adapter for the reminders list + val remindersAdapter = RemindersListAdapter(requireContext(), model) + binding.alertsList.adapter = remindersAdapter + + // When the new reminder button is tapped, create a new blank one + binding.newAlertButton.setOnClickListener { + val newList = (model.reminders.value ?: emptyList()) + .toMutableList() + .apply { add(CalendarReminder.DEFAULT) } + model.reminders.postValue(newList) } + + // When the reminders are updated, submit the new list to the recycler view adapter + model.reminders.observe(viewLifecycleOwner) { remindersAdapter.submitList(it) } + + return binding.root } - class TitleColorModel: ViewModel() { + class TitleColorModel : ViewModel() { var url = MutableLiveData() var originalTitle: String? = null @@ -51,9 +70,134 @@ class TitleColorFragment: Fragment() { var originalColor: Int? = null val color = MutableLiveData() + var originalIgnoreAlerts: Boolean? = null + val ignoreAlerts = MutableLiveData() + + var originalReminders: List? = null + val reminders = MutableLiveData>() + + var allowedReminders = MutableLiveData>() + fun dirty() = - originalTitle != title.value || - originalColor != color.value + originalTitle != title.value || originalColor != color.value || originalIgnoreAlerts != ignoreAlerts.value || originalReminders != reminders.value + } + + /** + * Stores all the reminders registered for a given calendar. + * @since 20221201 + */ + data class CalendarReminder( + /** + * How much time to notify before the event. The unit of this value is determined by [method]. + * @since 20221201 + */ + val time: Long, + /** + * The unit to be used with [time]. The possible values are: + * - `0`: minutes (x1) + * - `1`: hours (x60) + * - `2`: days (x1440) + * + * This is an index, that also match the value at [R.array.add_calendar_alerts_item_units], this way the selection in the spinner is easier. + * @since 20221201 + * @see minutes + */ + @androidx.annotation.IntRange(from = 0, to = 2) + val units: Int, + @Method + val method: Int, + ) { + companion object { + val DEFAULT: CalendarReminder + get() = CalendarReminder(15, 0, CalendarContract.Reminders.METHOD_DEFAULT) + } + + @IntDef( + CalendarContract.Reminders.METHOD_DEFAULT, + CalendarContract.Reminders.METHOD_ALERT, + CalendarContract.Reminders.METHOD_EMAIL, + CalendarContract.Reminders.METHOD_SMS, + CalendarContract.Reminders.METHOD_ALARM, + ) + annotation class Method + + /** + * Provides the [time] specified, adjusted to match the amount of minutes. + * @since 20221202 + */ + val minutes: Long + get() = when (units) { + 0 -> time * 1 + 1 -> time * 60 + else -> time * 1440 + } + } + + class RemindersListAdapter(private val context: Context, private val model: TitleColorModel) : + ListAdapter(object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CalendarReminder, newItem: CalendarReminder): Boolean = + oldItem.time == newItem.time && oldItem.units == newItem.units && oldItem.method == newItem.method + + override fun areContentsTheSame(oldItem: CalendarReminder, newItem: CalendarReminder): Boolean = + oldItem.time == newItem.time && oldItem.units == newItem.units && oldItem.method == newItem.method + }) { + private val layoutInflater = LayoutInflater.from(context) + + class ViewHolder(val binding: AlertRowBinding) : LifecycleViewHolder(binding) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + Log.i(Constants.TAG, "Creating view holder") + val binding = AlertRowBinding.inflate(layoutInflater, parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val reminder = currentList[position] + val binding = holder.binding + + ArrayAdapter.createFromResource(context, R.array.add_calendar_alerts_item_units, android.R.layout.simple_spinner_item).also { adapter -> + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.unitsSpinner.adapter = adapter + } + model.allowedReminders.observe(holder.lifecycleOwner!!) { allowedReminders -> + val methods = context.resources.getStringArray(R.array.add_calendar_alerts_item_methods) + val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, allowedReminders.map { methods[it] }) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.methodSpinner.adapter = adapter + } + + binding.delete.setOnClickListener { + // Remove the item at the current position + val newList = currentList.toMutableList().apply { removeAt(position) } + model.reminders.postValue(newList) + } + + // Update the currently selected values + binding.time.setText(reminder.minutes.toString()) + binding.unitsSpinner.setSelection(reminder.units) + binding.methodSpinner.setSelection(reminder.method) + + // Add validation listeners + binding.time.addTextChangedListener { text -> + // Make sure the value entered is an int + val number = text?.toString()?.toIntOrNull() + binding.time.error = if (number == null) + context.getString(R.string.add_calendar_alerts_error_number) + else if (number < 15) + context.getString(R.string.add_calendar_alerts_error_min) + else + null + } + // TODO: Make sure that the selected method is supported + binding.methodSpinner.onItemSelectedListener = object : OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) { /* Ignore */ + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + + } + } + } } } From 69173653524b66af65033ae45e4270167d143306 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:34:32 +0100 Subject: [PATCH 06/32] Added allowed reminders passing Signed-off-by: Arnau Mora --- .../main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index 0dc60138..bdb24953 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -149,6 +149,9 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.originalColor = it titleColorModel.color.value = it } + calendar.allowedReminders.let { + titleColorModel.allowedReminders.postValue(it) + } model.active.value = calendar.isSynced From 2d6609d9fe765a62f1596300cc6ca627d3f60246 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:34:47 +0100 Subject: [PATCH 07/32] Added `ColorPickerActivity.Contract` Signed-off-by: Arnau Mora --- .../java/at/bitfire/icsdroid/ui/ColorPickerActivity.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/ColorPickerActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/ColorPickerActivity.kt index b6eff4d6..a2f0e0ad 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/ColorPickerActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/ColorPickerActivity.kt @@ -4,8 +4,10 @@ package at.bitfire.icsdroid.ui +import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContract import androidx.appcompat.app.AppCompatActivity import at.bitfire.icsdroid.db.LocalCalendar import com.jaredrummler.android.colorpicker.ColorPickerDialog @@ -17,6 +19,14 @@ class ColorPickerActivity: AppCompatActivity(), ColorPickerDialogListener { const val EXTRA_COLOR = "color" } + class Contract: ActivityResultContract() { + override fun createIntent(context: Context, input: Int?): Intent = Intent(context, ColorPickerActivity::class.java).apply { + putExtra(EXTRA_COLOR, input) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Int = intent?.getIntExtra(EXTRA_COLOR, LocalCalendar.DEFAULT_COLOR) ?: LocalCalendar.DEFAULT_COLOR + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From c9bb20bd8e648a6a782cf056477cb8b38c705fb6 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:34:58 +0100 Subject: [PATCH 08/32] Added strings Signed-off-by: Arnau Mora --- app/src/main/res/values/strings.xml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41a286d9..6d6d333d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,31 @@ Pick file User name Validating calendar resource… + Alerts + Ignore alerts embed in the calendar + Custom Alerts + Add custom alerts that will be applied to every event in the calendar. + + minutes + hours + days + + before, by + + + (default) + an alert + an email + SMS + an alarm + + Deletes the reminder + Adds a new alert + Must be a number + Select a greater value Share details From 7cae6848ade7125d144674be81404be19da9c005 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:57:53 +0100 Subject: [PATCH 09/32] Added reminders and ignore alerts storage Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 37 ++++++++++--- .../icsdroid/ui/EditCalendarActivity.kt | 15 +++++- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 52 +------------------ 3 files changed, 46 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index aeb9ed66..aedf1700 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -11,12 +11,10 @@ import android.content.ContentValues import android.os.RemoteException import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Events -import android.util.Log import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidCalendarFactory import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter -import at.bitfire.icsdroid.Constants class LocalCalendar private constructor( account: Account, @@ -34,6 +32,19 @@ class LocalCalendar private constructor( const val COLUMN_ERROR_MESSAGE = Calendars.CAL_SYNC6 const val COLUMN_ALLOWED_REMINDERS = Calendars.ALLOWED_REMINDERS + /** + * Stores if the calendar's embed alerts should be ignored. + * @since 20221202 + */ + const val COLUMN_IGNORE_EMBED = Calendars.CAL_SYNC8 + + /** + * Stores all the reminders added for all the events in the calendar. It's a list of elements separated by ;, and parsed into [CalendarReminder] with + * [CalendarReminder.parse] (Uses "," as divider). + * @since 20221202 + */ + const val COLUMN_REMINDERS = Calendars.CAL_SYNC7 + fun findById(account: Account, provider: ContentProviderClient, id: Long) = findByID(account, provider, Factory, id) @@ -49,7 +60,10 @@ class LocalCalendar private constructor( var lastSync = 0L // time of last sync (0 if none) var errorMessage: String? = null // error message (HTTP status or exception name) of last sync (or null) - var allowedReminders: List = listOf() + var ignoreEmbedAlerts: Boolean? = null + + var allowedReminders: List = emptyList() + var reminders: List = emptyList() override fun populate(info: ContentValues) { @@ -66,7 +80,14 @@ class LocalCalendar private constructor( ?.split(',') ?.mapNotNull { it.toIntOrNull() } ?.let { allowedReminders = it } - Log.i(Constants.TAG, "Allowed reminders: $allowedReminders") + + info.getAsString(COLUMN_REMINDERS) + ?.split(';') + ?.map { CalendarReminder.parse(it) } + ?.let { reminders = it } + + info.getAsBoolean(COLUMN_IGNORE_EMBED) + ?.let { ignoreEmbedAlerts = it } } fun updateStatusSuccess(eTag: String?, lastModified: Long) { @@ -74,12 +95,14 @@ class LocalCalendar private constructor( this.lastModified = lastModified lastSync = System.currentTimeMillis() - val values = ContentValues(5) + val values = ContentValues(7) values.put(COLUMN_ETAG, eTag) values.put(COLUMN_LAST_MODIFIED, lastModified) values.put(COLUMN_LAST_SYNC, lastSync) values.putNull(COLUMN_ERROR_MESSAGE) values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) + values.put(COLUMN_REMINDERS, reminders.serialize()) + values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) update(values) } @@ -97,12 +120,14 @@ class LocalCalendar private constructor( lastSync = System.currentTimeMillis() errorMessage = message - val values = ContentValues(5) + val values = ContentValues(7) values.putNull(COLUMN_ETAG) values.putNull(COLUMN_LAST_MODIFIED) values.put(COLUMN_LAST_SYNC, lastSync) values.put(COLUMN_ERROR_MESSAGE, message) values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) + values.put(COLUMN_REMINDERS, reminders.serialize()) + values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) update(values) } diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index bdb24953..38fe811e 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -37,6 +37,7 @@ import at.bitfire.icsdroid.R import at.bitfire.icsdroid.databinding.EditCalendarBinding import at.bitfire.icsdroid.db.CalendarCredentials import at.bitfire.icsdroid.db.LocalCalendar +import at.bitfire.icsdroid.db.serialize import java.io.FileNotFoundException class EditCalendarActivity: AppCompatActivity() { @@ -70,6 +71,8 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.title.observe(this, invalidate) titleColorModel.color.observe(this, invalidate) + titleColorModel.ignoreAlerts.observe(this, invalidate) + titleColorModel.reminders.observe(this, invalidate) credentialsModel.requiresAuth.observe(this, invalidate) credentialsModel.username.observe(this, invalidate) @@ -149,6 +152,14 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.originalColor = it titleColorModel.color.value = it } + calendar.ignoreEmbedAlerts.let { + titleColorModel.originalIgnoreAlerts = it + titleColorModel.ignoreAlerts.postValue(it) + } + calendar.reminders.let { + titleColorModel.originalReminders = it + titleColorModel.reminders.postValue(it) + } calendar.allowedReminders.let { titleColorModel.allowedReminders.postValue(it) } @@ -182,10 +193,12 @@ class EditCalendarActivity: AppCompatActivity() { var success = false model.calendar.value?.let { calendar -> try { - val values = ContentValues(3) + val values = ContentValues(4) values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, titleColorModel.title.value) values.put(CalendarContract.Calendars.CALENDAR_COLOR, titleColorModel.color.value) values.put(CalendarContract.Calendars.SYNC_EVENTS, if (model.active.value == true) 1 else 0) + values.put(LocalCalendar.COLUMN_REMINDERS, titleColorModel.reminders.value?.serialize()) + values.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) calendar.update(values) credentialsModel.let { model -> diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index d8136f0f..d9b0b5c2 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -26,6 +26,7 @@ import at.bitfire.icsdroid.Constants import at.bitfire.icsdroid.R import at.bitfire.icsdroid.databinding.AlertRowBinding import at.bitfire.icsdroid.databinding.TitleColorBinding +import at.bitfire.icsdroid.db.CalendarReminder class TitleColorFragment : Fragment() { @@ -82,57 +83,6 @@ class TitleColorFragment : Fragment() { originalTitle != title.value || originalColor != color.value || originalIgnoreAlerts != ignoreAlerts.value || originalReminders != reminders.value } - /** - * Stores all the reminders registered for a given calendar. - * @since 20221201 - */ - data class CalendarReminder( - /** - * How much time to notify before the event. The unit of this value is determined by [method]. - * @since 20221201 - */ - val time: Long, - /** - * The unit to be used with [time]. The possible values are: - * - `0`: minutes (x1) - * - `1`: hours (x60) - * - `2`: days (x1440) - * - * This is an index, that also match the value at [R.array.add_calendar_alerts_item_units], this way the selection in the spinner is easier. - * @since 20221201 - * @see minutes - */ - @androidx.annotation.IntRange(from = 0, to = 2) - val units: Int, - @Method - val method: Int, - ) { - companion object { - val DEFAULT: CalendarReminder - get() = CalendarReminder(15, 0, CalendarContract.Reminders.METHOD_DEFAULT) - } - - @IntDef( - CalendarContract.Reminders.METHOD_DEFAULT, - CalendarContract.Reminders.METHOD_ALERT, - CalendarContract.Reminders.METHOD_EMAIL, - CalendarContract.Reminders.METHOD_SMS, - CalendarContract.Reminders.METHOD_ALARM, - ) - annotation class Method - - /** - * Provides the [time] specified, adjusted to match the amount of minutes. - * @since 20221202 - */ - val minutes: Long - get() = when (units) { - 0 -> time * 1 - 1 -> time * 60 - else -> time * 1440 - } - } - class RemindersListAdapter(private val context: Context, private val model: TitleColorModel) : ListAdapter(object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: CalendarReminder, newItem: CalendarReminder): Boolean = From 3b77d7706eeef4afc2383b46d0a2c3ad6ab185ec Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 01:58:02 +0100 Subject: [PATCH 10/32] Moved class declaration Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/db/CalendarReminder.kt | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt diff --git a/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt b/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt new file mode 100644 index 00000000..b65b77cd --- /dev/null +++ b/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt @@ -0,0 +1,95 @@ +package at.bitfire.icsdroid.db + +import android.provider.CalendarContract +import androidx.annotation.IntDef +import at.bitfire.icsdroid.R + +/** + * Stores all the reminders registered for a given calendar. + * @since 20221201 + */ +data class CalendarReminder( + /** + * How much time to notify before the event. The unit of this value is determined by [method]. + * @since 20221201 + */ + val time: Long, + /** + * The unit to be used with [time]. The possible values are: + * - `0`: minutes (x1) + * - `1`: hours (x60) + * - `2`: days (x1440) + * + * This is an index, that also match the value at [R.array.add_calendar_alerts_item_units], this way the selection in the spinner is easier. + * @since 20221201 + * @see minutes + */ + @androidx.annotation.IntRange(from = 0, to = 2) + val units: Int, + @Method + val method: Int, +) { + companion object { + val DEFAULT: CalendarReminder + get() = CalendarReminder(15, 0, CalendarContract.Reminders.METHOD_DEFAULT) + + /** + * Converts back into a [CalendarReminder] the contents that have been serialized with [CalendarReminder.serialize]. + * @author Arnau Mora + * @since 20221202 + * @param string The text to convert. + * @return An initialized instance of [CalendarReminder] with the data provided by [string]. + * @throws IllegalArgumentException When the given [string] is not valid. Usually because the length of the parameters is not correct, or because one or + * more parameters could not be converted back to Long/Int. + * @see serialize + */ + @Throws(IllegalArgumentException::class) + fun parse(string: String): CalendarReminder = string.split(",").let { pieces -> + if (pieces.size != 3) + throw IllegalArgumentException("The provided string is not valid ({Long},{Int},{Int}): $string") + val time = pieces[0].toLongOrNull() + val units = pieces[1].toIntOrNull() + val method = pieces[2].toIntOrNull() + if (time == null || units == null || method == null) + throw IllegalArgumentException("The provided string is not valid ({Long},{Int},{Int}): $string") + + CalendarReminder(time, units, method) + } + } + + @IntDef( + CalendarContract.Reminders.METHOD_DEFAULT, + CalendarContract.Reminders.METHOD_ALERT, + CalendarContract.Reminders.METHOD_EMAIL, + CalendarContract.Reminders.METHOD_SMS, + CalendarContract.Reminders.METHOD_ALARM, + ) + annotation class Method + + /** + * Provides the [time] specified, adjusted to match the amount of minutes. + * @since 20221202 + */ + val minutes: Long + get() = when (units) { + 0 -> time * 1 + 1 -> time * 60 + else -> time * 1440 + } + + /** + * Converts the data in the class into a [String] that then can be converted back again into a [CalendarReminder]. + * @author Arnau Mora + * @since 20221202 + * @return The fields of the class turned into a [String]. + * @see parse + */ + fun serialize(): String = arrayOf(time, units, method).joinToString(",") +} + +/** + * Maps each element of the collection into a [String] with [CalendarReminder.serialize] and joins all the resulting elements with `;`. + * @author Arnau Mora + * @since 20221202 + */ +fun Iterable.serialize() = joinToString(";") { it.serialize() } From 0b0405010a4c361d844d596495d9ceb02f210628 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 02:20:44 +0100 Subject: [PATCH 11/32] Added storage of custom alarm preferences Signed-off-by: Arnau Mora --- .../java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt index 7ca26664..c127ee94 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt @@ -23,6 +23,7 @@ import at.bitfire.icsdroid.Constants import at.bitfire.icsdroid.R import at.bitfire.icsdroid.db.CalendarCredentials import at.bitfire.icsdroid.db.LocalCalendar +import at.bitfire.icsdroid.db.serialize class AddCalendarDetailsFragment: Fragment() { @@ -77,6 +78,8 @@ class AddCalendarDetailsFragment: Fragment() { calInfo.put(Calendars.SYNC_EVENTS, 1) calInfo.put(Calendars.VISIBLE, 1) calInfo.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) + calInfo.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) + calInfo.put(LocalCalendar.COLUMN_REMINDERS, titleColorModel.reminders.value?.serialize()) val client: ContentProviderClient? = requireActivity().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY) return try { From f8dcca81c864603648db8ddfafd19b2c377f50c6 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 02:20:58 +0100 Subject: [PATCH 12/32] Added check for blank reminders Signed-off-by: Arnau Mora --- app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index aedf1700..ffe19e6f 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -82,6 +82,7 @@ class LocalCalendar private constructor( ?.let { allowedReminders = it } info.getAsString(COLUMN_REMINDERS) + ?.takeIf { it.isNotBlank() } ?.split(';') ?.map { CalendarReminder.parse(it) } ?.let { reminders = it } From a52b5a3ef1af1014b3ad8d75b1683cb69bb5d459 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 02:21:29 +0100 Subject: [PATCH 13/32] Added alarms removal when loading Signed-off-by: Arnau Mora --- app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt index af989e50..d85b92f5 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt @@ -65,6 +65,13 @@ class ProcessEventsTask( InputStreamReader(data, contentType?.charset() ?: Charsets.UTF_8).use { reader -> try { val events = Event.eventsFromReader(reader) + .map { + if (calendar.ignoreEmbedAlerts == true) { + Log.d(Constants.TAG, "Removing all alarms from ${it.uid}") + it.alarms.clear() + } + it + } processEvents(events) Log.i(Constants.TAG, "Calendar sync successful, ETag=$eTag, lastModified=$lastModified") From daa8f39de18162a14eed195574e97199ef57b481 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 11:58:35 +0100 Subject: [PATCH 14/32] Simplified calendar alerts to just one Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/db/CalendarReminder.kt | 51 ++---- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 18 +-- .../icsdroid/ui/AddCalendarDetailsFragment.kt | 3 +- .../icsdroid/ui/EditCalendarActivity.kt | 11 +- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 148 +++++++----------- app/src/main/res/layout/alert_row.xml | 48 ------ app/src/main/res/layout/title_color.xml | 48 ++++-- 7 files changed, 119 insertions(+), 208 deletions(-) delete mode 100644 app/src/main/res/layout/alert_row.xml diff --git a/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt b/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt index b65b77cd..bffcc455 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt @@ -10,28 +10,16 @@ import at.bitfire.icsdroid.R */ data class CalendarReminder( /** - * How much time to notify before the event. The unit of this value is determined by [method]. + * How many minutes to alert before the event. * @since 20221201 */ - val time: Long, - /** - * The unit to be used with [time]. The possible values are: - * - `0`: minutes (x1) - * - `1`: hours (x60) - * - `2`: days (x1440) - * - * This is an index, that also match the value at [R.array.add_calendar_alerts_item_units], this way the selection in the spinner is easier. - * @since 20221201 - * @see minutes - */ - @androidx.annotation.IntRange(from = 0, to = 2) - val units: Int, + val minutes: Long, @Method val method: Int, ) { companion object { val DEFAULT: CalendarReminder - get() = CalendarReminder(15, 0, CalendarContract.Reminders.METHOD_DEFAULT) + get() = CalendarReminder(15, CalendarContract.Reminders.METHOD_DEFAULT) /** * Converts back into a [CalendarReminder] the contents that have been serialized with [CalendarReminder.serialize]. @@ -45,15 +33,14 @@ data class CalendarReminder( */ @Throws(IllegalArgumentException::class) fun parse(string: String): CalendarReminder = string.split(",").let { pieces -> - if (pieces.size != 3) - throw IllegalArgumentException("The provided string is not valid ({Long},{Int},{Int}): $string") + if (pieces.size != 2) + throw IllegalArgumentException("The provided string is not valid ({Long},{Int}): $string") val time = pieces[0].toLongOrNull() - val units = pieces[1].toIntOrNull() - val method = pieces[2].toIntOrNull() - if (time == null || units == null || method == null) - throw IllegalArgumentException("The provided string is not valid ({Long},{Int},{Int}): $string") + val method = pieces[1].toIntOrNull() + if (time == null || method == null) + throw IllegalArgumentException("The provided string is not valid ({Long},{Int}): $string") - CalendarReminder(time, units, method) + CalendarReminder(time, method) } } @@ -66,17 +53,6 @@ data class CalendarReminder( ) annotation class Method - /** - * Provides the [time] specified, adjusted to match the amount of minutes. - * @since 20221202 - */ - val minutes: Long - get() = when (units) { - 0 -> time * 1 - 1 -> time * 60 - else -> time * 1440 - } - /** * Converts the data in the class into a [String] that then can be converted back again into a [CalendarReminder]. * @author Arnau Mora @@ -84,12 +60,5 @@ data class CalendarReminder( * @return The fields of the class turned into a [String]. * @see parse */ - fun serialize(): String = arrayOf(time, units, method).joinToString(",") + fun serialize(): String = arrayOf(minutes, method).joinToString(",") } - -/** - * Maps each element of the collection into a [String] with [CalendarReminder.serialize] and joins all the resulting elements with `;`. - * @author Arnau Mora - * @since 20221202 - */ -fun Iterable.serialize() = joinToString(";") { it.serialize() } diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index ffe19e6f..5daae6ba 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -39,11 +39,10 @@ class LocalCalendar private constructor( const val COLUMN_IGNORE_EMBED = Calendars.CAL_SYNC8 /** - * Stores all the reminders added for all the events in the calendar. It's a list of elements separated by ;, and parsed into [CalendarReminder] with - * [CalendarReminder.parse] (Uses "," as divider). + * Stores the reminder set for all the events of the calendar. * @since 20221202 */ - const val COLUMN_REMINDERS = Calendars.CAL_SYNC7 + const val COLUMN_REMINDER = Calendars.CAL_SYNC7 fun findById(account: Account, provider: ContentProviderClient, id: Long) = findByID(account, provider, Factory, id) @@ -63,7 +62,7 @@ class LocalCalendar private constructor( var ignoreEmbedAlerts: Boolean? = null var allowedReminders: List = emptyList() - var reminders: List = emptyList() + var reminder: CalendarReminder? = null override fun populate(info: ContentValues) { @@ -81,11 +80,10 @@ class LocalCalendar private constructor( ?.mapNotNull { it.toIntOrNull() } ?.let { allowedReminders = it } - info.getAsString(COLUMN_REMINDERS) + info.getAsString(COLUMN_REMINDER) ?.takeIf { it.isNotBlank() } - ?.split(';') - ?.map { CalendarReminder.parse(it) } - ?.let { reminders = it } + ?.let { CalendarReminder.parse(it) } + ?.let { reminder = it } info.getAsBoolean(COLUMN_IGNORE_EMBED) ?.let { ignoreEmbedAlerts = it } @@ -102,7 +100,7 @@ class LocalCalendar private constructor( values.put(COLUMN_LAST_SYNC, lastSync) values.putNull(COLUMN_ERROR_MESSAGE) values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) - values.put(COLUMN_REMINDERS, reminders.serialize()) + values.put(COLUMN_REMINDER, reminder?.serialize()) values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) update(values) } @@ -127,7 +125,7 @@ class LocalCalendar private constructor( values.put(COLUMN_LAST_SYNC, lastSync) values.put(COLUMN_ERROR_MESSAGE, message) values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) - values.put(COLUMN_REMINDERS, reminders.serialize()) + values.put(COLUMN_REMINDER, reminder?.serialize()) values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) update(values) } diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt index c127ee94..97b8779e 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt @@ -23,7 +23,6 @@ import at.bitfire.icsdroid.Constants import at.bitfire.icsdroid.R import at.bitfire.icsdroid.db.CalendarCredentials import at.bitfire.icsdroid.db.LocalCalendar -import at.bitfire.icsdroid.db.serialize class AddCalendarDetailsFragment: Fragment() { @@ -79,7 +78,7 @@ class AddCalendarDetailsFragment: Fragment() { calInfo.put(Calendars.VISIBLE, 1) calInfo.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) calInfo.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) - calInfo.put(LocalCalendar.COLUMN_REMINDERS, titleColorModel.reminders.value?.serialize()) + calInfo.put(LocalCalendar.COLUMN_REMINDER, titleColorModel.reminder.value?.serialize()) val client: ContentProviderClient? = requireActivity().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY) return try { diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index 38fe811e..196a8d8c 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -37,7 +37,6 @@ import at.bitfire.icsdroid.R import at.bitfire.icsdroid.databinding.EditCalendarBinding import at.bitfire.icsdroid.db.CalendarCredentials import at.bitfire.icsdroid.db.LocalCalendar -import at.bitfire.icsdroid.db.serialize import java.io.FileNotFoundException class EditCalendarActivity: AppCompatActivity() { @@ -72,7 +71,7 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.title.observe(this, invalidate) titleColorModel.color.observe(this, invalidate) titleColorModel.ignoreAlerts.observe(this, invalidate) - titleColorModel.reminders.observe(this, invalidate) + titleColorModel.reminder.observe(this, invalidate) credentialsModel.requiresAuth.observe(this, invalidate) credentialsModel.username.observe(this, invalidate) @@ -156,9 +155,9 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.originalIgnoreAlerts = it titleColorModel.ignoreAlerts.postValue(it) } - calendar.reminders.let { - titleColorModel.originalReminders = it - titleColorModel.reminders.postValue(it) + calendar.reminder.let { + titleColorModel.originalReminder = it + titleColorModel.reminder.postValue(it) } calendar.allowedReminders.let { titleColorModel.allowedReminders.postValue(it) @@ -197,7 +196,7 @@ class EditCalendarActivity: AppCompatActivity() { values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, titleColorModel.title.value) values.put(CalendarContract.Calendars.CALENDAR_COLOR, titleColorModel.color.value) values.put(CalendarContract.Calendars.SYNC_EVENTS, if (model.active.value == true) 1 else 0) - values.put(LocalCalendar.COLUMN_REMINDERS, titleColorModel.reminders.value?.serialize()) + values.put(LocalCalendar.COLUMN_REMINDER, titleColorModel.reminder.value?.serialize()) values.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) calendar.update(values) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index d9b0b5c2..861522c4 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -4,27 +4,19 @@ package at.bitfire.icsdroid.ui -import android.content.Context import android.os.Bundle -import android.provider.CalendarContract -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter -import androidx.annotation.IntDef import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import at.bitfire.icsdroid.Constants import at.bitfire.icsdroid.R -import at.bitfire.icsdroid.databinding.AlertRowBinding import at.bitfire.icsdroid.databinding.TitleColorBinding import at.bitfire.icsdroid.db.CalendarReminder @@ -44,20 +36,59 @@ class TitleColorFragment : Fragment() { // Listener for launching the color picker binding.color.setOnClickListener { colorPickerContract.launch(model.color.value) } - // Initialize the adapter for the reminders list - val remindersAdapter = RemindersListAdapter(requireContext(), model) - binding.alertsList.adapter = remindersAdapter + binding.customAlertEnable.setOnCheckedChangeListener { _, checked -> + if (!checked) + model.reminder.postValue(null) + else + model.reminder.postValue(model.originalReminder ?: CalendarReminder.DEFAULT) + } + binding.customAlertTime.addTextChangedListener { text -> + // Make sure the value entered is an int + val number = text?.toString()?.toLongOrNull() + binding.customAlertTime.error = when (number) { + null -> getString(R.string.add_calendar_alerts_error_number) + else -> null + } + number?.let { + val reminder = model.reminder.value ?: CalendarReminder.DEFAULT + val newReminder = reminder.copy(minutes = it) + // Only update model if the reminder has been changed + if (reminder != newReminder) + model.reminder.postValue(newReminder) + } + } + binding.customAlertMethod.onItemSelectedListener = object : OnItemSelectedListener { + override fun onItemSelected(adapter: AdapterView<*>?, view: View?, position: Int, id: Long) { + val reminder = model.reminder.value ?: CalendarReminder.DEFAULT + model.allowedReminders.value?.let { + model.reminder.postValue(reminder.copy(method = it[position])) + } + } - // When the new reminder button is tapped, create a new blank one - binding.newAlertButton.setOnClickListener { - val newList = (model.reminders.value ?: emptyList()) - .toMutableList() - .apply { add(CalendarReminder.DEFAULT) } - model.reminders.postValue(newList) + override fun onNothingSelected(adapter: AdapterView<*>?) {} } - // When the reminders are updated, submit the new list to the recycler view adapter - model.reminders.observe(viewLifecycleOwner) { remindersAdapter.submitList(it) } + model.allowedReminders.observe(viewLifecycleOwner) { allowedReminders -> + val methods = resources.getStringArray(R.array.add_calendar_alerts_custom_methods) + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, allowedReminders.map { methods[it] }) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.customAlertMethod.adapter = adapter + } + + model.reminder.observe(viewLifecycleOwner) { reminder -> + if (reminder == null) { + binding.customAlertCard.visibility = View.GONE + binding.customAlertEnable.isChecked = false + } else { + val minutes = reminder.minutes + binding.customAlertCard.visibility = View.VISIBLE + binding.customAlertEnable.isChecked = true + binding.customAlertTime + .takeIf { it.text.toString() != minutes.toString() } + ?.setText(minutes.toString()) + binding.customAlertMethod.setSelection(reminder.method) + } + } return binding.root } @@ -74,80 +105,17 @@ class TitleColorFragment : Fragment() { var originalIgnoreAlerts: Boolean? = null val ignoreAlerts = MutableLiveData() - var originalReminders: List? = null - val reminders = MutableLiveData>() + var originalReminder: CalendarReminder? = null + val reminder = MutableLiveData() var allowedReminders = MutableLiveData>() - fun dirty() = - originalTitle != title.value || originalColor != color.value || originalIgnoreAlerts != ignoreAlerts.value || originalReminders != reminders.value - } - - class RemindersListAdapter(private val context: Context, private val model: TitleColorModel) : - ListAdapter(object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: CalendarReminder, newItem: CalendarReminder): Boolean = - oldItem.time == newItem.time && oldItem.units == newItem.units && oldItem.method == newItem.method - - override fun areContentsTheSame(oldItem: CalendarReminder, newItem: CalendarReminder): Boolean = - oldItem.time == newItem.time && oldItem.units == newItem.units && oldItem.method == newItem.method - }) { - private val layoutInflater = LayoutInflater.from(context) - - class ViewHolder(val binding: AlertRowBinding) : LifecycleViewHolder(binding) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - Log.i(Constants.TAG, "Creating view holder") - val binding = AlertRowBinding.inflate(layoutInflater, parent, false) - return ViewHolder(binding) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val reminder = currentList[position] - val binding = holder.binding - - ArrayAdapter.createFromResource(context, R.array.add_calendar_alerts_item_units, android.R.layout.simple_spinner_item).also { adapter -> - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.unitsSpinner.adapter = adapter - } - model.allowedReminders.observe(holder.lifecycleOwner!!) { allowedReminders -> - val methods = context.resources.getStringArray(R.array.add_calendar_alerts_item_methods) - val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, allowedReminders.map { methods[it] }) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.methodSpinner.adapter = adapter - } - - binding.delete.setOnClickListener { - // Remove the item at the current position - val newList = currentList.toMutableList().apply { removeAt(position) } - model.reminders.postValue(newList) - } - - // Update the currently selected values - binding.time.setText(reminder.minutes.toString()) - binding.unitsSpinner.setSelection(reminder.units) - binding.methodSpinner.setSelection(reminder.method) - - // Add validation listeners - binding.time.addTextChangedListener { text -> - // Make sure the value entered is an int - val number = text?.toString()?.toIntOrNull() - binding.time.error = if (number == null) - context.getString(R.string.add_calendar_alerts_error_number) - else if (number < 15) - context.getString(R.string.add_calendar_alerts_error_min) - else - null - } - // TODO: Make sure that the selected method is supported - binding.methodSpinner.onItemSelectedListener = object : OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) { /* Ignore */ - } - - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - - } - } - } + fun dirty() = arrayOf( + originalTitle to title, + originalColor to color, + originalIgnoreAlerts to ignoreAlerts, + originalReminder to reminder, + ).any { (original, state) -> original != state.value } } } diff --git a/app/src/main/res/layout/alert_row.xml b/app/src/main/res/layout/alert_row.xml deleted file mode 100644 index 7331daf8..00000000 --- a/app/src/main/res/layout/alert_row.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/title_color.xml b/app/src/main/res/layout/title_color.xml index 8611f9f9..77fda9ab 100644 --- a/app/src/main/res/layout/title_color.xml +++ b/app/src/main/res/layout/title_color.xml @@ -108,29 +108,55 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/add_calendar_alerts_list_title" + android:text="@string/add_calendar_alerts_custom_title" android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" /> - + android:layout_height="wrap_content" /> - + android:elevation="5dp" + android:orientation="horizontal"> + + + + + + + + + + + From 76a04056d767a719b598f58cbc77280838e517ce Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 2 Dec 2022 12:00:42 +0100 Subject: [PATCH 15/32] Updated strings Signed-off-by: Arnau Mora --- app/src/main/res/values/strings.xml | 32 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d6d333d..d3df64a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,29 +44,27 @@ Validating calendar resource… Alerts Ignore alerts embed in the calendar - Custom Alerts - Add custom alerts that will be applied to every event in the calendar. - - minutes - hours - days - - before, by - + Custom Alert + Set a custom alert for all the events of the calendar.¡ + minutes before, by + (default) + an alert + an email + SMS + an alarm + - (default) - an alert - an email - SMS - an alarm + @string/alert_method_default + @string/alert_method_alert + @string/alert_method_email + @string/alert_method_sms + @string/alert_method_alarm - Deletes the reminder - Adds a new alert Must be a number - Select a greater value Share details From af8136155e6e589fa08436b1825e654a197e5641 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 10:58:37 +0100 Subject: [PATCH 16/32] Simplified UI to have only one default alarm Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/ProcessEventsTask.kt | 7 - .../at/bitfire/icsdroid/db/LocalCalendar.kt | 122 +++++++++++++---- .../icsdroid/ui/AddCalendarDetailsFragment.kt | 7 +- .../icsdroid/ui/EditCalendarActivity.kt | 13 +- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 128 +++++++++++------- app/src/main/res/layout/title_color.xml | 95 +++---------- 6 files changed, 198 insertions(+), 174 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt index d85b92f5..af989e50 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt @@ -65,13 +65,6 @@ class ProcessEventsTask( InputStreamReader(data, contentType?.charset() ?: Charsets.UTF_8).use { reader -> try { val events = Event.eventsFromReader(reader) - .map { - if (calendar.ignoreEmbedAlerts == true) { - Log.d(Constants.TAG, "Removing all alarms from ${it.uid}") - it.alarms.clear() - } - it - } processEvents(events) Log.i(Constants.TAG, "Calendar sync successful, ETag=$eTag, lastModified=$lastModified") diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index 5daae6ba..6c2d42bd 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -11,16 +11,26 @@ import android.content.ContentValues import android.os.RemoteException import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Events +import android.util.Log import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidCalendarFactory import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.icsdroid.Constants +import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.Repeat +import net.fortuna.ical4j.model.property.Trigger +import java.time.temporal.TemporalAmount class LocalCalendar private constructor( - account: Account, - provider: ContentProviderClient, - id: Long -): AndroidCalendar(account, provider, LocalEvent.Factory, id) { + account: Account, + provider: ContentProviderClient, + id: Long +) : AndroidCalendar(account, provider, LocalEvent.Factory, id) { companion object { @@ -39,16 +49,17 @@ class LocalCalendar private constructor( const val COLUMN_IGNORE_EMBED = Calendars.CAL_SYNC8 /** + * TODO: Change javadoc * Stores the reminder set for all the events of the calendar. * @since 20221202 */ - const val COLUMN_REMINDER = Calendars.CAL_SYNC7 + const val COLUMN_DEFAULT_ALARM = Calendars.CAL_SYNC7 fun findById(account: Account, provider: ContentProviderClient, id: Long) = - findByID(account, provider, Factory, id) + findByID(account, provider, Factory, id) fun findAll(account: Account, provider: ContentProviderClient) = - find(account, provider, Factory, null, null) + find(account, provider, Factory, null, null) } @@ -61,8 +72,7 @@ class LocalCalendar private constructor( var ignoreEmbedAlerts: Boolean? = null - var allowedReminders: List = emptyList() - var reminder: CalendarReminder? = null + var defaultAlarmMinutes: Long? = null override fun populate(info: ContentValues) { @@ -75,20 +85,68 @@ class LocalCalendar private constructor( info.getAsLong(COLUMN_LAST_SYNC)?.let { lastSync = it } errorMessage = info.getAsString(COLUMN_ERROR_MESSAGE) - info.getAsString(COLUMN_ALLOWED_REMINDERS) - ?.split(',') - ?.mapNotNull { it.toIntOrNull() } - ?.let { allowedReminders = it } - - info.getAsString(COLUMN_REMINDER) - ?.takeIf { it.isNotBlank() } - ?.let { CalendarReminder.parse(it) } - ?.let { reminder = it } + info.getAsLong(COLUMN_DEFAULT_ALARM) + ?.let { defaultAlarmMinutes = it } info.getAsBoolean(COLUMN_IGNORE_EMBED) ?.let { ignoreEmbedAlerts = it } } + private fun updateAlarms() { + queryEvents(null, null) + .also { if (ignoreEmbedAlerts == true) Log.d(Constants.TAG, "Removing all alarms for ${it.size} events.") } + .filter { it.event != null } + .forEach { ev -> + val event = ev.event!! + if (ignoreEmbedAlerts == true) { + // Remove all alerts + event.unknownProperties.add( + PropertyBuilder() + .name("--ALARMS-BAK") + .value( + event.alarms.joinToString(";;") { it.toString() } + ) + .build() + ) + Log.d(Constants.TAG, "Removing all alarms from ${event.uid}: $event") + event.alarms.clear() + ev.update(event) + } else { + // Add all the alarms back again + Log.d(Constants.TAG, "Adding all disabled alarms") + val props = event.unknownProperties + .find { it.name == "--ALARMS-BAK" } + ?.value + ?.split(Regex(";{2}")) + ?.map { alarmString -> + val properties = PropertyList() + properties.addAll( + alarmString.split('\n') + .map { it.split(':') } + .mapNotNull { + val value = it.subList(1, it.size).joinToString(":") + when (it[0]) { + // TODO: Fix trigger parsing + /*Property.TRIGGER -> { + Log.d(Constants.TAG, "Trigger: \"$value\"") + val adapter = TemporalAmountAdapter.parse(value) + Trigger(adapter.duration) + }*/ + Property.ACTION -> Action(value) + Property.DESCRIPTION -> Description(value) + Property.REPEAT -> Repeat(value.toInt()) + Property.DURATION -> Duration(TemporalAmountAdapter.parse(value).duration) + else -> null + } + } + ) + VAlarm.Factory().createComponent(properties) + } + Log.d(Constants.TAG, "Props: $props") + } + } + } + fun updateStatusSuccess(eTag: String?, lastModified: Long) { this.eTag = eTag this.lastModified = lastModified @@ -99,10 +157,11 @@ class LocalCalendar private constructor( values.put(COLUMN_LAST_MODIFIED, lastModified) values.put(COLUMN_LAST_SYNC, lastSync) values.putNull(COLUMN_ERROR_MESSAGE) - values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) - values.put(COLUMN_REMINDER, reminder?.serialize()) + values.put(COLUMN_DEFAULT_ALARM, defaultAlarmMinutes) values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) update(values) + + updateAlarms() } fun updateStatusNotModified() { @@ -111,6 +170,8 @@ class LocalCalendar private constructor( val values = ContentValues(1) values.put(COLUMN_LAST_SYNC, lastSync) update(values) + + updateAlarms() } fun updateStatusError(message: String) { @@ -124,8 +185,7 @@ class LocalCalendar private constructor( values.putNull(COLUMN_LAST_MODIFIED) values.put(COLUMN_LAST_SYNC, lastSync) values.put(COLUMN_ERROR_MESSAGE, message) - values.put(COLUMN_ALLOWED_REMINDERS, allowedReminders.joinToString(",")) - values.put(COLUMN_REMINDER, reminder?.serialize()) + values.put(COLUMN_DEFAULT_ALARM, defaultAlarmMinutes) values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) update(values) } @@ -139,36 +199,38 @@ class LocalCalendar private constructor( } fun queryByUID(uid: String) = - queryEvents("${Events._SYNC_ID}=?", arrayOf(uid)) + queryEvents("${Events._SYNC_ID}=?", arrayOf(uid)) fun retainByUID(uids: MutableSet): Int { var deleted = 0 try { - provider.query(Events.CONTENT_URI.asSyncAdapter(account), - arrayOf(Events._ID, Events._SYNC_ID, Events.ORIGINAL_SYNC_ID), - "${Events.CALENDAR_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS NULL", arrayOf(id.toString()), null)?.use { row -> + provider.query( + Events.CONTENT_URI.asSyncAdapter(account), + arrayOf(Events._ID, Events._SYNC_ID, Events.ORIGINAL_SYNC_ID), + "${Events.CALENDAR_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS NULL", arrayOf(id.toString()), null + )?.use { row -> while (row.moveToNext()) { val eventId = row.getLong(0) val syncId = row.getString(1) if (!uids.contains(syncId)) { provider.delete(ContentUris.withAppendedId(Events.CONTENT_URI, eventId).asSyncAdapter(account), null, null) deleted++ - + uids -= syncId } } } return deleted - } catch(e: RemoteException) { + } catch (e: RemoteException) { throw CalendarStorageException("Couldn't delete local events") } } - object Factory: AndroidCalendarFactory { + object Factory : AndroidCalendarFactory { override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) = - LocalCalendar(account, provider, id) + LocalCalendar(account, provider, id) } diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt index 97b8779e..24d3e6cf 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt @@ -37,6 +37,11 @@ class AddCalendarDetailsFragment: Fragment() { } titleColorModel.title.observe(this, invalidateOptionsMenu) titleColorModel.color.observe(this, invalidateOptionsMenu) + titleColorModel.ignoreAlerts.observe(this, invalidateOptionsMenu) + titleColorModel.defaultAlarmMinutes.observe(this, invalidateOptionsMenu) + + // Set the default value to null so that the visibility of the summary is updated + titleColorModel.defaultAlarmMinutes.postValue(null) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, inState: Bundle?): View { @@ -78,7 +83,7 @@ class AddCalendarDetailsFragment: Fragment() { calInfo.put(Calendars.VISIBLE, 1) calInfo.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) calInfo.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) - calInfo.put(LocalCalendar.COLUMN_REMINDER, titleColorModel.reminder.value?.serialize()) + calInfo.put(LocalCalendar.COLUMN_DEFAULT_ALARM, titleColorModel.defaultAlarmMinutes.value) val client: ContentProviderClient? = requireActivity().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY) return try { diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index 196a8d8c..3b338178 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -71,7 +71,7 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.title.observe(this, invalidate) titleColorModel.color.observe(this, invalidate) titleColorModel.ignoreAlerts.observe(this, invalidate) - titleColorModel.reminder.observe(this, invalidate) + titleColorModel.defaultAlarmMinutes.observe(this, invalidate) credentialsModel.requiresAuth.observe(this, invalidate) credentialsModel.username.observe(this, invalidate) @@ -155,12 +155,9 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.originalIgnoreAlerts = it titleColorModel.ignoreAlerts.postValue(it) } - calendar.reminder.let { - titleColorModel.originalReminder = it - titleColorModel.reminder.postValue(it) - } - calendar.allowedReminders.let { - titleColorModel.allowedReminders.postValue(it) + calendar.defaultAlarmMinutes.let { + titleColorModel.originalDefaultAlarmMinutes = it + titleColorModel.defaultAlarmMinutes.postValue(it) } model.active.value = calendar.isSynced @@ -196,7 +193,7 @@ class EditCalendarActivity: AppCompatActivity() { values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, titleColorModel.title.value) values.put(CalendarContract.Calendars.CALENDAR_COLOR, titleColorModel.color.value) values.put(CalendarContract.Calendars.SYNC_EVENTS, if (model.active.value == true) 1 else 0) - values.put(LocalCalendar.COLUMN_REMINDER, titleColorModel.reminder.value?.serialize()) + values.put(LocalCalendar.COLUMN_DEFAULT_ALARM, titleColorModel.defaultAlarmMinutes.value) values.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) calendar.update(values) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index 861522c4..3e7a537f 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -4,13 +4,18 @@ package at.bitfire.icsdroid.ui +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.LinearLayout +import androidx.core.view.marginStart import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -19,6 +24,13 @@ import androidx.lifecycle.ViewModel import at.bitfire.icsdroid.R import at.bitfire.icsdroid.databinding.TitleColorBinding import at.bitfire.icsdroid.db.CalendarReminder +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlin.math.roundToInt + +const val MINUTES_IN_AN_HOUR = 60f +const val MINUTES_IN_A_DAY = 24*60f +const val MINUTES_IN_AN_WEEK = 7*24*60f +const val MINUTES_IN_A_MONTH = 30*7*24*60f class TitleColorFragment : Fragment() { @@ -33,61 +45,75 @@ class TitleColorFragment : Fragment() { binding.lifecycleOwner = this binding.model = model - // Listener for launching the color picker - binding.color.setOnClickListener { colorPickerContract.launch(model.color.value) } + var firstCheck = true - binding.customAlertEnable.setOnCheckedChangeListener { _, checked -> - if (!checked) - model.reminder.postValue(null) - else - model.reminder.postValue(model.originalReminder ?: CalendarReminder.DEFAULT) - } - binding.customAlertTime.addTextChangedListener { text -> - // Make sure the value entered is an int - val number = text?.toString()?.toLongOrNull() - binding.customAlertTime.error = when (number) { - null -> getString(R.string.add_calendar_alerts_error_number) - else -> null + model.defaultAlarmMinutes.observe(viewLifecycleOwner) { min: Long? -> + binding.defaultAlarmSwitch.isChecked = min != null + firstCheck = false + if (min == null) { + binding.defaultAlarmText.visibility = View.GONE + return@observe } - number?.let { - val reminder = model.reminder.value ?: CalendarReminder.DEFAULT - val newReminder = reminder.copy(minutes = it) - // Only update model if the reminder has been changed - if (reminder != newReminder) - model.reminder.postValue(newReminder) + // TODO: Build string to have exactly the duration specified. e.g.: 1 hour and 37 minutes + val minutes = min.toInt() + val text = if (minutes < MINUTES_IN_AN_HOUR) + resources.getQuantityString(R.plurals.add_calendar_alarms_custom_minutes, minutes, minutes) + else if (minutes < MINUTES_IN_A_DAY) (minutes / MINUTES_IN_AN_HOUR).roundToInt().let { + resources.getQuantityString(R.plurals.add_calendar_alarms_custom_hours, it, it) } - } - binding.customAlertMethod.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected(adapter: AdapterView<*>?, view: View?, position: Int, id: Long) { - val reminder = model.reminder.value ?: CalendarReminder.DEFAULT - model.allowedReminders.value?.let { - model.reminder.postValue(reminder.copy(method = it[position])) - } + else if (minutes < MINUTES_IN_AN_WEEK) (minutes / MINUTES_IN_A_DAY).roundToInt().let { + resources.getQuantityString(R.plurals.add_calendar_alarms_custom_days, it, it) } - - override fun onNothingSelected(adapter: AdapterView<*>?) {} + else if (minutes < MINUTES_IN_A_MONTH) (minutes / MINUTES_IN_AN_WEEK).roundToInt().let { + resources.getQuantityString(R.plurals.add_calendar_alarms_custom_weeks, it, it) + } + else (minutes / MINUTES_IN_A_MONTH).roundToInt().let { + resources.getQuantityString(R.plurals.add_calendar_alarms_custom_months, it, it) + } + binding.defaultAlarmText.text = getString(R.string.add_calendar_alarms_default_description, text) + binding.defaultAlarmText.visibility = View.VISIBLE } - model.allowedReminders.observe(viewLifecycleOwner) { allowedReminders -> - val methods = resources.getStringArray(R.array.add_calendar_alerts_custom_methods) - val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, allowedReminders.map { methods[it] }) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.customAlertMethod.adapter = adapter - } + // Listener for launching the color picker + binding.color.setOnClickListener { colorPickerContract.launch(model.color.value) } + + binding.defaultAlarmSwitch.setOnCheckedChangeListener { _, checked -> + if (firstCheck) return@setOnCheckedChangeListener + + if (!checked) { + model.defaultAlarmMinutes.postValue(null) + return@setOnCheckedChangeListener + } - model.reminder.observe(viewLifecycleOwner) { reminder -> - if (reminder == null) { - binding.customAlertCard.visibility = View.GONE - binding.customAlertEnable.isChecked = false - } else { - val minutes = reminder.minutes - binding.customAlertCard.visibility = View.VISIBLE - binding.customAlertEnable.isChecked = true - binding.customAlertTime - .takeIf { it.text.toString() != minutes.toString() } - ?.setText(minutes.toString()) - binding.customAlertMethod.setSelection(reminder.method) + val editText = EditText(requireContext()).apply { + setHint(R.string.default_alarm_dialog_hint) + + addTextChangedListener { txt -> + val text = txt?.toString() + val num = text?.toLongOrNull() + error = if (text == null || text.isBlank() || num == null) + getString(R.string.default_alarm_dialog_error) + else + null + } } + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.default_alarm_dialog_title) + .setMessage(R.string.default_alarm_dialog_message) + .setView(editText) + .setPositiveButton(R.string.default_alarm_dialog_set) { dialog, _ -> + if (editText.error != null) { + // TODO: Value introduced is not valid + } else { + model.defaultAlarmMinutes.postValue(editText.text?.toString()?.toLongOrNull()) + dialog.dismiss() + } + } + .setOnCancelListener { + binding.defaultAlarmSwitch.isChecked = false + } + .create() + .show() } return binding.root @@ -105,16 +131,14 @@ class TitleColorFragment : Fragment() { var originalIgnoreAlerts: Boolean? = null val ignoreAlerts = MutableLiveData() - var originalReminder: CalendarReminder? = null - val reminder = MutableLiveData() - - var allowedReminders = MutableLiveData>() + var originalDefaultAlarmMinutes: Long? = null + val defaultAlarmMinutes = MutableLiveData() fun dirty() = arrayOf( originalTitle to title, originalColor to color, originalIgnoreAlerts to ignoreAlerts, - originalReminder to reminder, + originalDefaultAlarmMinutes to defaultAlarmMinutes, ).any { (original, state) -> original != state.value } } diff --git a/app/src/main/res/layout/title_color.xml b/app/src/main/res/layout/title_color.xml index 77fda9ab..6673aaa9 100644 --- a/app/src/main/res/layout/title_color.xml +++ b/app/src/main/res/layout/title_color.xml @@ -73,93 +73,36 @@ + android:text="@string/add_calendar_alarms_ignore_title" /> - - - - - - - - - - - - + style="@style/TextAppearance.MaterialComponents.Caption" + android:text="@string/add_calendar_alarms_ignore_description" /> - - - - - - - - - - - - + - - + \ No newline at end of file From ef4aec4165cc8287b2f568cb5627dc344d2b6718 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 10:58:48 +0100 Subject: [PATCH 17/32] Added new strings Signed-off-by: Arnau Mora --- app/src/main/res/values/strings.xml | 53 ++++++++++++++++------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3df64a3..70919890 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,29 +42,36 @@ Pick file User name Validating calendar resource… - Alerts - Ignore alerts embed in the calendar - Custom Alert - Set a custom alert for all the events of the calendar.¡ - minutes before, by - (default) - an alert - an email - SMS - an alarm - - - @string/alert_method_default - @string/alert_method_alert - @string/alert_method_email - @string/alert_method_sms - @string/alert_method_alarm - - Must be a number + Alarms + Ignore alerts embed in the calendar + If enabled, all the incoming alarms from the server will be dismissed. + Add a default alarm for all events + Alarms set to %s before + + %d minute + %d minutes + + + %d hour + %d hours + + + %d day + %d days + + + %d week + %d days + + + %d month + %d months + + Add default alarm + This will add an alarm for all events + Minutes before event + Set + Introduce a valid number Share details From 691345efb77bfae2982a4ec583bf06f0946a4273 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 10:59:47 +0100 Subject: [PATCH 18/32] Removed no longer necessary `CalendarReminder` Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/db/CalendarReminder.kt | 64 ------------------- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 8 --- 2 files changed, 72 deletions(-) delete mode 100644 app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt diff --git a/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt b/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt deleted file mode 100644 index bffcc455..00000000 --- a/app/src/main/java/at/bitfire/icsdroid/db/CalendarReminder.kt +++ /dev/null @@ -1,64 +0,0 @@ -package at.bitfire.icsdroid.db - -import android.provider.CalendarContract -import androidx.annotation.IntDef -import at.bitfire.icsdroid.R - -/** - * Stores all the reminders registered for a given calendar. - * @since 20221201 - */ -data class CalendarReminder( - /** - * How many minutes to alert before the event. - * @since 20221201 - */ - val minutes: Long, - @Method - val method: Int, -) { - companion object { - val DEFAULT: CalendarReminder - get() = CalendarReminder(15, CalendarContract.Reminders.METHOD_DEFAULT) - - /** - * Converts back into a [CalendarReminder] the contents that have been serialized with [CalendarReminder.serialize]. - * @author Arnau Mora - * @since 20221202 - * @param string The text to convert. - * @return An initialized instance of [CalendarReminder] with the data provided by [string]. - * @throws IllegalArgumentException When the given [string] is not valid. Usually because the length of the parameters is not correct, or because one or - * more parameters could not be converted back to Long/Int. - * @see serialize - */ - @Throws(IllegalArgumentException::class) - fun parse(string: String): CalendarReminder = string.split(",").let { pieces -> - if (pieces.size != 2) - throw IllegalArgumentException("The provided string is not valid ({Long},{Int}): $string") - val time = pieces[0].toLongOrNull() - val method = pieces[1].toIntOrNull() - if (time == null || method == null) - throw IllegalArgumentException("The provided string is not valid ({Long},{Int}): $string") - - CalendarReminder(time, method) - } - } - - @IntDef( - CalendarContract.Reminders.METHOD_DEFAULT, - CalendarContract.Reminders.METHOD_ALERT, - CalendarContract.Reminders.METHOD_EMAIL, - CalendarContract.Reminders.METHOD_SMS, - CalendarContract.Reminders.METHOD_ALARM, - ) - annotation class Method - - /** - * Converts the data in the class into a [String] that then can be converted back again into a [CalendarReminder]. - * @author Arnau Mora - * @since 20221202 - * @return The fields of the class turned into a [String]. - * @see parse - */ - fun serialize(): String = arrayOf(minutes, method).joinToString(",") -} diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index 3e7a537f..db867d82 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -4,18 +4,11 @@ package at.bitfire.icsdroid.ui -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager -import android.widget.AdapterView -import android.widget.AdapterView.OnItemSelectedListener -import android.widget.ArrayAdapter import android.widget.EditText -import android.widget.LinearLayout -import androidx.core.view.marginStart import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -23,7 +16,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import at.bitfire.icsdroid.R import at.bitfire.icsdroid.databinding.TitleColorBinding -import at.bitfire.icsdroid.db.CalendarReminder import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlin.math.roundToInt From 2d5c2336a7f65582ddf5d4d0171c35ad7f263a8b Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 11:05:26 +0100 Subject: [PATCH 19/32] Sketched workflow Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index 6c2d42bd..965d2c03 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -100,49 +100,13 @@ class LocalCalendar private constructor( val event = ev.event!! if (ignoreEmbedAlerts == true) { // Remove all alerts - event.unknownProperties.add( - PropertyBuilder() - .name("--ALARMS-BAK") - .value( - event.alarms.joinToString(";;") { it.toString() } - ) - .build() - ) Log.d(Constants.TAG, "Removing all alarms from ${event.uid}: $event") event.alarms.clear() ev.update(event) } else { // Add all the alarms back again Log.d(Constants.TAG, "Adding all disabled alarms") - val props = event.unknownProperties - .find { it.name == "--ALARMS-BAK" } - ?.value - ?.split(Regex(";{2}")) - ?.map { alarmString -> - val properties = PropertyList() - properties.addAll( - alarmString.split('\n') - .map { it.split(':') } - .mapNotNull { - val value = it.subList(1, it.size).joinToString(":") - when (it[0]) { - // TODO: Fix trigger parsing - /*Property.TRIGGER -> { - Log.d(Constants.TAG, "Trigger: \"$value\"") - val adapter = TemporalAmountAdapter.parse(value) - Trigger(adapter.duration) - }*/ - Property.ACTION -> Action(value) - Property.DESCRIPTION -> Description(value) - Property.REPEAT -> Repeat(value.toInt()) - Property.DURATION -> Duration(TemporalAmountAdapter.parse(value).duration) - else -> null - } - } - ) - VAlarm.Factory().createComponent(properties) - } - Log.d(Constants.TAG, "Props: $props") + // TODO: Fetch all alarms again from server } } } From c265be33fd68217efd3dc15db49909ccbd6f3b06 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 11:13:24 +0100 Subject: [PATCH 20/32] Implemented default alarm adder Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index 965d2c03..a2405eff 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -49,8 +49,7 @@ class LocalCalendar private constructor( const val COLUMN_IGNORE_EMBED = Calendars.CAL_SYNC8 /** - * TODO: Change javadoc - * Stores the reminder set for all the events of the calendar. + * Stores the default alarm to set to all events in the given calendar. * @since 20221202 */ const val COLUMN_DEFAULT_ALARM = Calendars.CAL_SYNC7 @@ -108,6 +107,23 @@ class LocalCalendar private constructor( Log.d(Constants.TAG, "Adding all disabled alarms") // TODO: Fetch all alarms again from server } + if (defaultAlarmMinutes != null) { + // Add the default alarm to the even + event.alarms.add( + // Create the new VAlarm + VAlarm.Factory().createComponent( + // Set all the properties for the alarm + PropertyList().apply { + // Set action to DISPLAY + add(Action.DISPLAY) + // Add the trigger x minutes before + add(Trigger(TemporalAmountAdapter.parse("-P${defaultAlarmMinutes}M").duration)) + // Set an empty description (maybe set something default?) + add(Description("")) + } + ) + ) + } } } From 8d2ffe25fb7d2ba34212f8fdebbc2db84b21e5dc Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 11:18:09 +0100 Subject: [PATCH 21/32] Just a small deprecation Signed-off-by: Arnau Mora --- .../main/java/at/bitfire/icsdroid/ui/CalendarListActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/CalendarListActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/CalendarListActivity.kt index ea2c95a4..8d40195d 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/CalendarListActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/CalendarListActivity.kt @@ -20,6 +20,7 @@ import android.view.* import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -56,7 +57,7 @@ class CalendarListActivity: AppCompatActivity(), SwipeRefreshLayout.OnRefreshLis binding.lifecycleOwner = this binding.model = model - val defaultRefreshColor = resources.getColor(R.color.lightblue) + val defaultRefreshColor = ContextCompat.getColor(this, R.color.lightblue) binding.refresh.setColorSchemeColors(defaultRefreshColor) binding.refresh.setOnRefreshListener(this) binding.refresh.setSize(SwipeRefreshLayout.LARGE) From 6c077b526bee088a7104ed1e3d69577cdd014bd1 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 11:59:16 +0100 Subject: [PATCH 22/32] Added ignore cache option Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/ProcessEventsTask.kt | 5 ++-- .../java/at/bitfire/icsdroid/SyncWorker.kt | 24 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt index af989e50..da96605c 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt @@ -26,7 +26,8 @@ import java.net.MalformedURLException class ProcessEventsTask( val context: Context, - val calendar: LocalCalendar + val calendar: LocalCalendar, + val ignoreCache: Boolean, ) { suspend fun sync() { @@ -65,7 +66,7 @@ class ProcessEventsTask( InputStreamReader(data, contentType?.charset() ?: Charsets.UTF_8).use { reader -> try { val events = Event.eventsFromReader(reader) - processEvents(events) + processEvents(events.map { if (ignoreCache) it.lastModified = null; it }) Log.i(Constants.TAG, "Calendar sync successful, ETag=$eTag, lastModified=$lastModified") calendar.updateStatusSuccess(eTag, lastModified ?: 0L) diff --git a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt index 7b047ca7..873b10e9 100644 --- a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt @@ -26,6 +26,8 @@ class SyncWorker( const val NAME = "SyncWorker" + const val IGNORE_CACHE = "IgnoreCache" + /** * Enqueues a sync job for immediate execution. If the sync is forced, @@ -33,9 +35,11 @@ class SyncWorker( * * @param context required for managing work * @param force *true* enqueues the sync regardless of the network state; *false* adds a [NetworkType.CONNECTED] constraint + * @param ignoreCache *true* ignores all locally stored data and fetched everything from the server again */ - fun run(context: Context, force: Boolean = false) { + fun run(context: Context, force: Boolean = false, ignoreCache: Boolean = false) { val request = OneTimeWorkRequestBuilder() + .setInputData(workDataOf(IGNORE_CACHE to ignoreCache)) val policy: ExistingWorkPolicy if (force) { @@ -67,10 +71,11 @@ class SyncWorker( @SuppressLint("Recycle") override suspend fun doWork(): Result { + val ignoreCache = inputData.getBoolean(IGNORE_CACHE, false) applicationContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { providerClient -> try { return withContext(Dispatchers.Default) { - performSync(AppAccount.get(applicationContext), providerClient) + performSync(AppAccount.get(applicationContext), providerClient, ignoreCache) } } finally { providerClient.closeCompat() @@ -79,12 +84,19 @@ class SyncWorker( return Result.failure() } - private suspend fun performSync(account: Account, provider: ContentProviderClient): Result { - Log.i(Constants.TAG, "Synchronizing ${account.name}") + private suspend fun performSync(account: Account, provider: ContentProviderClient, ignoreCache: Boolean): Result { + Log.i(Constants.TAG, "Synchronizing ${account.name}. Ignore cache: $ignoreCache") try { LocalCalendar.findAll(account, provider) - .filter { it.isSynced } - .forEach { ProcessEventsTask(applicationContext, it).sync() } + .map { + if (ignoreCache) { + it.lastModified = 0 + it.eTag = null + } + it + } + .filter { it.isSynced } + .forEach { ProcessEventsTask(applicationContext, it, ignoreCache).sync() } } catch (e: CalendarStorageException) { Log.e(Constants.TAG, "Calendar storage exception", e) From 207c939cf871948fa4b5cca0ae4c127810d8e91e Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 11:59:34 +0100 Subject: [PATCH 23/32] Added ignore cache refresh of saving Signed-off-by: Arnau Mora --- .../java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index 3b338178..d7cf6e29 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -30,10 +30,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import at.bitfire.ical4android.CalendarStorageException -import at.bitfire.icsdroid.AppAccount -import at.bitfire.icsdroid.Constants -import at.bitfire.icsdroid.HttpUtils -import at.bitfire.icsdroid.R +import at.bitfire.icsdroid.* import at.bitfire.icsdroid.databinding.EditCalendarBinding import at.bitfire.icsdroid.db.CalendarCredentials import at.bitfire.icsdroid.db.LocalCalendar @@ -197,6 +194,8 @@ class EditCalendarActivity: AppCompatActivity() { values.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) calendar.update(values) + SyncWorker.run(this, ignoreCache = true) + credentialsModel.let { model -> val credentials = CalendarCredentials(this) if (model.requiresAuth.value == true) From ce254da7eb8d0c653f703a04b1b2f2d4cec99d2d Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 5 Dec 2022 12:06:36 +0100 Subject: [PATCH 24/32] Optimized event updating Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index a2405eff..86c0bd4e 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -21,10 +21,8 @@ import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.Description -import net.fortuna.ical4j.model.property.Duration -import net.fortuna.ical4j.model.property.Repeat import net.fortuna.ical4j.model.property.Trigger -import java.time.temporal.TemporalAmount +import java.time.Duration class LocalCalendar private constructor( account: Account, @@ -101,14 +99,13 @@ class LocalCalendar private constructor( // Remove all alerts Log.d(Constants.TAG, "Removing all alarms from ${event.uid}: $event") event.alarms.clear() - ev.update(event) - } else { - // Add all the alarms back again - Log.d(Constants.TAG, "Adding all disabled alarms") - // TODO: Fetch all alarms again from server } - if (defaultAlarmMinutes != null) { - // Add the default alarm to the even + defaultAlarmMinutes?.let { minutes -> + // Check if already added alarm + val alarm = event.alarms.find { it.description.value.contains("*added by ICSx5") } + if (alarm != null) return@let + // Add the default alarm to the event + Log.d(Constants.TAG, "Adding the default alarm to ${event.uid}.") event.alarms.add( // Create the new VAlarm VAlarm.Factory().createComponent( @@ -117,13 +114,15 @@ class LocalCalendar private constructor( // Set action to DISPLAY add(Action.DISPLAY) // Add the trigger x minutes before - add(Trigger(TemporalAmountAdapter.parse("-P${defaultAlarmMinutes}M").duration)) - // Set an empty description (maybe set something default?) - add(Description("")) + val duration = Duration.ofMinutes(minutes * -1) + add(Trigger(duration)) + // Set a default description for checking if added + add(Description("*added by ICSx5")) } ) ) } + ev.update(event) } } From 42744ba243115b495668b7cf995a36377e6b9084 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 6 Dec 2022 20:26:48 +0100 Subject: [PATCH 25/32] Make forceResync explicit --- .../at/bitfire/icsdroid/ProcessEventsTask.kt | 34 +++++++++++++------ .../java/at/bitfire/icsdroid/SyncWorker.kt | 21 ++++-------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt index da96605c..b65a07e8 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt @@ -24,10 +24,24 @@ import java.io.InputStream import java.io.InputStreamReader import java.net.MalformedURLException +/** + * Fetches the .ics for a given Webcal subscription and stores the events + * in the local calendar provider. + * + * By default, caches will be used: + * + * - for fetching a calendar by HTTP (ETag/Last-Modified), + * - for updating the local events (will only be updated when LAST-MODIFIED is newer). + * + * @param context context to work in + * @param calendar represents the subscription to be checked + * @param forceResync enforces that the calendar is fetched and all events are fully processed + * (useful when subscription settings have been changed) + */ class ProcessEventsTask( - val context: Context, - val calendar: LocalCalendar, - val ignoreCache: Boolean, + val context: Context, + val calendar: LocalCalendar, + val forceResync: Boolean ) { suspend fun sync() { @@ -54,7 +68,7 @@ class ProcessEventsTask( calendar.updateStatusError(e.localizedMessage ?: e.toString()) return } - Log.i(Constants.TAG, "Synchronizing $uri") + Log.i(Constants.TAG, "Synchronizing $uri, forceResync=$forceResync") // dismiss old notifications val notificationManager = NotificationUtils.createChannels(context) @@ -66,7 +80,7 @@ class ProcessEventsTask( InputStreamReader(data, contentType?.charset() ?: Charsets.UTF_8).use { reader -> try { val events = Event.eventsFromReader(reader) - processEvents(events.map { if (ignoreCache) it.lastModified = null; it }) + processEvents(events, forceResync) Log.i(Constants.TAG, "Calendar sync successful, ETag=$eTag, lastModified=$lastModified") calendar.updateStatusSuccess(eTag, lastModified ?: 0L) @@ -99,9 +113,9 @@ class ProcessEventsTask( downloader.password = password } - if (calendar.eTag != null) + if (calendar.eTag != null && !forceResync) downloader.ifNoneMatch = calendar.eTag - if (calendar.lastModified != 0L) + if (calendar.lastModified != 0L && !forceResync) downloader.ifModifiedSince = calendar.lastModified downloader.fetch() @@ -132,8 +146,8 @@ class ProcessEventsTask( } } - private fun processEvents(events: List) { - Log.i(Constants.TAG, "Processing ${events.size} events") + private fun processEvents(events: List, ignoreLastModified: Boolean) { + Log.i(Constants.TAG, "Processing ${events.size} events (ignoreLastModified=$ignoreLastModified)") val uids = HashSet(events.size) for (event in events) { @@ -148,7 +162,7 @@ class ProcessEventsTask( } else { val localEvent = localEvents.first() - var lastModified = event.lastModified + var lastModified = if (ignoreLastModified) null else event.lastModified Log.d(Constants.TAG, "$uid already in local calendar, lastModified = $lastModified") if (lastModified != null) { diff --git a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt index 873b10e9..1f85279e 100644 --- a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt @@ -26,7 +26,7 @@ class SyncWorker( const val NAME = "SyncWorker" - const val IGNORE_CACHE = "IgnoreCache" + const val FORCE_RESYNC = "forceResync" /** @@ -39,7 +39,7 @@ class SyncWorker( */ fun run(context: Context, force: Boolean = false, ignoreCache: Boolean = false) { val request = OneTimeWorkRequestBuilder() - .setInputData(workDataOf(IGNORE_CACHE to ignoreCache)) + .setInputData(workDataOf(FORCE_RESYNC to ignoreCache)) val policy: ExistingWorkPolicy if (force) { @@ -71,11 +71,11 @@ class SyncWorker( @SuppressLint("Recycle") override suspend fun doWork(): Result { - val ignoreCache = inputData.getBoolean(IGNORE_CACHE, false) + val forceResync = inputData.getBoolean(FORCE_RESYNC, false) applicationContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { providerClient -> try { return withContext(Dispatchers.Default) { - performSync(AppAccount.get(applicationContext), providerClient, ignoreCache) + performSync(AppAccount.get(applicationContext), providerClient, forceResync) } } finally { providerClient.closeCompat() @@ -84,19 +84,12 @@ class SyncWorker( return Result.failure() } - private suspend fun performSync(account: Account, provider: ContentProviderClient, ignoreCache: Boolean): Result { - Log.i(Constants.TAG, "Synchronizing ${account.name}. Ignore cache: $ignoreCache") + private suspend fun performSync(account: Account, provider: ContentProviderClient, forceResync: Boolean): Result { + Log.i(Constants.TAG, "Synchronizing ${account.name} (forceResync=$forceResync)") try { LocalCalendar.findAll(account, provider) - .map { - if (ignoreCache) { - it.lastModified = 0 - it.eTag = null - } - it - } .filter { it.isSynced } - .forEach { ProcessEventsTask(applicationContext, it, ignoreCache).sync() } + .forEach { ProcessEventsTask(applicationContext, it, forceResync).sync() } } catch (e: CalendarStorageException) { Log.e(Constants.TAG, "Calendar storage exception", e) From b45f78c58283883de407a68519332e70f4af68d0 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 6 Dec 2022 20:36:21 +0100 Subject: [PATCH 26/32] Minor changes --- .../java/at/bitfire/icsdroid/SyncWorker.kt | 10 ++-- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 53 +++++++++++-------- .../icsdroid/ui/AddCalendarDetailsFragment.kt | 2 +- .../icsdroid/ui/EditCalendarActivity.kt | 6 +-- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt index 1f85279e..f739d7d0 100644 --- a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt @@ -33,13 +33,13 @@ class SyncWorker( * Enqueues a sync job for immediate execution. If the sync is forced, * the "requires network connection" constraint won't be set. * - * @param context required for managing work - * @param force *true* enqueues the sync regardless of the network state; *false* adds a [NetworkType.CONNECTED] constraint - * @param ignoreCache *true* ignores all locally stored data and fetched everything from the server again + * @param context required for managing work + * @param force *true* enqueues the sync regardless of the network state; *false* adds a [NetworkType.CONNECTED] constraint + * @param forceResync *true* ignores all locally stored data and fetched everything from the server again */ - fun run(context: Context, force: Boolean = false, ignoreCache: Boolean = false) { + fun run(context: Context, force: Boolean = false, forceResync: Boolean = false) { val request = OneTimeWorkRequestBuilder() - .setInputData(workDataOf(FORCE_RESYNC to ignoreCache)) + .setInputData(workDataOf(FORCE_RESYNC to forceResync)) val policy: ExistingWorkPolicy if (force) { diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index 86c0bd4e..4e950e40 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -17,7 +17,8 @@ import at.bitfire.ical4android.AndroidCalendarFactory import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import at.bitfire.icsdroid.Constants -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.Description @@ -38,13 +39,12 @@ class LocalCalendar private constructor( const val COLUMN_LAST_MODIFIED = Calendars.CAL_SYNC4 const val COLUMN_LAST_SYNC = Calendars.CAL_SYNC5 const val COLUMN_ERROR_MESSAGE = Calendars.CAL_SYNC6 - const val COLUMN_ALLOWED_REMINDERS = Calendars.ALLOWED_REMINDERS /** - * Stores if the calendar's embed alerts should be ignored. + * Stores if the calendar's embedded alerts should be ignored. * @since 20221202 */ - const val COLUMN_IGNORE_EMBED = Calendars.CAL_SYNC8 + const val COLUMN_IGNORE_EMBEDDED = Calendars.CAL_SYNC8 /** * Stores the default alarm to set to all events in the given calendar. @@ -60,15 +60,22 @@ class LocalCalendar private constructor( } - var url: String? = null // URL of iCalendar file - var eTag: String? = null // iCalendar ETag at last successful sync - - var lastModified = 0L // iCalendar Last-Modified at last successful sync (or 0 for none) - var lastSync = 0L // time of last sync (0 if none) - var errorMessage: String? = null // error message (HTTP status or exception name) of last sync (or null) - - var ignoreEmbedAlerts: Boolean? = null - + /** URL of iCalendar file */ + var url: String? = null + /** iCalendar ETag at last successful sync */ + var eTag: String? = null + + /** iCalendar Last-Modified at last successful sync (or 0 for none) */ + var lastModified = 0L + /** time of last sync (0 if none) */ + var lastSync = 0L + /** error message (HTTP status or exception name) of last sync (or null) */ + var errorMessage: String? = null + + /** Setting: whether to ignore alarms embedded in the Webcal */ + var ignoreEmbeddedAlerts: Boolean? = null + /** Setting: Shall a default alarm be added to every event in the calendar? If yes, this + * field contains the minutes before the event. If no, it is *null*. */ var defaultAlarmMinutes: Long? = null @@ -82,24 +89,24 @@ class LocalCalendar private constructor( info.getAsLong(COLUMN_LAST_SYNC)?.let { lastSync = it } errorMessage = info.getAsString(COLUMN_ERROR_MESSAGE) - info.getAsLong(COLUMN_DEFAULT_ALARM) - ?.let { defaultAlarmMinutes = it } - - info.getAsBoolean(COLUMN_IGNORE_EMBED) - ?.let { ignoreEmbedAlerts = it } + info.getAsBoolean(COLUMN_IGNORE_EMBEDDED)?.let { ignoreEmbeddedAlerts = it } + info.getAsLong(COLUMN_DEFAULT_ALARM)?.let { defaultAlarmMinutes = it } } private fun updateAlarms() { queryEvents(null, null) - .also { if (ignoreEmbedAlerts == true) Log.d(Constants.TAG, "Removing all alarms for ${it.size} events.") } + .also { if (ignoreEmbeddedAlerts == true) Log.d(Constants.TAG, "Removing all alarms for ${it.size} events.") } .filter { it.event != null } .forEach { ev -> val event = ev.event!! - if (ignoreEmbedAlerts == true) { - // Remove all alerts + + // according to setting: remove all alerts for every event + if (ignoreEmbeddedAlerts == true) { Log.d(Constants.TAG, "Removing all alarms from ${event.uid}: $event") event.alarms.clear() } + + // according to setting: add default alarm for every event defaultAlarmMinutes?.let { minutes -> // Check if already added alarm val alarm = event.alarms.find { it.description.value.contains("*added by ICSx5") } @@ -137,7 +144,7 @@ class LocalCalendar private constructor( values.put(COLUMN_LAST_SYNC, lastSync) values.putNull(COLUMN_ERROR_MESSAGE) values.put(COLUMN_DEFAULT_ALARM, defaultAlarmMinutes) - values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) + values.put(COLUMN_IGNORE_EMBEDDED, ignoreEmbeddedAlerts) update(values) updateAlarms() @@ -165,7 +172,7 @@ class LocalCalendar private constructor( values.put(COLUMN_LAST_SYNC, lastSync) values.put(COLUMN_ERROR_MESSAGE, message) values.put(COLUMN_DEFAULT_ALARM, defaultAlarmMinutes) - values.put(COLUMN_IGNORE_EMBED, ignoreEmbedAlerts) + values.put(COLUMN_IGNORE_EMBEDDED, ignoreEmbeddedAlerts) update(values) } diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt index 24d3e6cf..5566166d 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/AddCalendarDetailsFragment.kt @@ -82,7 +82,7 @@ class AddCalendarDetailsFragment: Fragment() { calInfo.put(Calendars.SYNC_EVENTS, 1) calInfo.put(Calendars.VISIBLE, 1) calInfo.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) - calInfo.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) + calInfo.put(LocalCalendar.COLUMN_IGNORE_EMBEDDED, titleColorModel.ignoreAlerts.value) calInfo.put(LocalCalendar.COLUMN_DEFAULT_ALARM, titleColorModel.defaultAlarmMinutes.value) val client: ContentProviderClient? = requireActivity().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index d7cf6e29..89893453 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -148,7 +148,7 @@ class EditCalendarActivity: AppCompatActivity() { titleColorModel.originalColor = it titleColorModel.color.value = it } - calendar.ignoreEmbedAlerts.let { + calendar.ignoreEmbeddedAlerts.let { titleColorModel.originalIgnoreAlerts = it titleColorModel.ignoreAlerts.postValue(it) } @@ -191,10 +191,10 @@ class EditCalendarActivity: AppCompatActivity() { values.put(CalendarContract.Calendars.CALENDAR_COLOR, titleColorModel.color.value) values.put(CalendarContract.Calendars.SYNC_EVENTS, if (model.active.value == true) 1 else 0) values.put(LocalCalendar.COLUMN_DEFAULT_ALARM, titleColorModel.defaultAlarmMinutes.value) - values.put(LocalCalendar.COLUMN_IGNORE_EMBED, titleColorModel.ignoreAlerts.value) + values.put(LocalCalendar.COLUMN_IGNORE_EMBEDDED, titleColorModel.ignoreAlerts.value) calendar.update(values) - SyncWorker.run(this, ignoreCache = true) + SyncWorker.run(this, forceResync = true) credentialsModel.let { model -> val credentials = CalendarCredentials(this) From 00440690463b6d71debc9beba4bf1c50c01c1a9e Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 8 Dec 2022 10:42:10 +0100 Subject: [PATCH 27/32] No longer used Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/ui/LifecycleViewHolder.kt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt b/app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt deleted file mode 100644 index 37967d59..00000000 --- a/app/src/main/java/at/bitfire/icsdroid/ui/LifecycleViewHolder.kt +++ /dev/null @@ -1,16 +0,0 @@ -package at.bitfire.icsdroid.ui - -import androidx.lifecycle.LifecycleOwner -import androidx.recyclerview.widget.RecyclerView -import androidx.viewbinding.ViewBinding - -/** - * A [RecyclerView.ViewHolder] that is aware of its [LifecycleOwner]. Also adapts directly the given [ViewBinding]. - * @since 20221202 - * @param binding The [ViewBinding] to give to the ViewHolder. - */ -abstract class LifecycleViewHolder (binding: B): RecyclerView.ViewHolder(binding.root) { - val lifecycleOwner by lazy { - binding.root.context as? LifecycleOwner - } -} From 4d66d24288154edface77c8ab1f6e19ed04061c5 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 8 Dec 2022 10:52:16 +0100 Subject: [PATCH 28/32] Moved alarms logic to `ProcessEventsTask` Signed-off-by: Arnau Mora --- .../at/bitfire/icsdroid/ProcessEventsTask.kt | 47 ++++++++++++++++++- .../at/bitfire/icsdroid/db/LocalCalendar.kt | 44 ----------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt index b65a07e8..97bc1acd 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt @@ -19,10 +19,17 @@ import at.bitfire.icsdroid.db.LocalCalendar import at.bitfire.icsdroid.db.LocalEvent import at.bitfire.icsdroid.ui.EditCalendarActivity import at.bitfire.icsdroid.ui.NotificationUtils +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.Trigger import okhttp3.MediaType import java.io.InputStream import java.io.InputStreamReader import java.net.MalformedURLException +import java.time.Duration /** * Fetches the .ics for a given Webcal subscription and stores the events @@ -59,6 +66,43 @@ class ProcessEventsTask( Log.i(Constants.TAG, "iCalendar file completely processed") } + /** + * Updates the alarms of the given event according to the [calendar]'s [LocalCalendar.defaultAlarmMinutes] and [LocalCalendar.ignoreEmbeddedAlerts] + * parameters. + * @since 20221208 + * @param event The event to update. + * @return The given [event], with the alarms updated. + */ + private fun updateAlarms(event: Event): Event = event.apply { + if (calendar.ignoreEmbeddedAlerts == true) { + // Remove all alerts + Log.d(Constants.TAG, "Removing all alarms from ${uid}: $this") + alarms.clear() + } + calendar.defaultAlarmMinutes?.let { minutes -> + // Check if already added alarm + val alarm = alarms.find { it.description.value.contains("*added by ICSx5") } + if (alarm != null) return@let + // Add the default alarm to the event + Log.d(Constants.TAG, "Adding the default alarm to ${uid}.") + alarms.add( + // Create the new VAlarm + VAlarm.Factory().createComponent( + // Set all the properties for the alarm + PropertyList().apply { + // Set action to DISPLAY + add(Action.DISPLAY) + // Add the trigger x minutes before + val duration = Duration.ofMinutes(minutes * -1) + add(Trigger(duration)) + // Set a default description for checking if added + add(Description("*added by ICSx5")) + } + ) + ) + } + } + private suspend fun processEvents() { val uri = try { @@ -150,7 +194,8 @@ class ProcessEventsTask( Log.i(Constants.TAG, "Processing ${events.size} events (ignoreLastModified=$ignoreLastModified)") val uids = HashSet(events.size) - for (event in events) { + for (ev in events) { + val event = updateAlarms(ev) val uid = event.uid!! Log.d(Constants.TAG, "Found VEVENT: $uid") uids += uid diff --git a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt index 4e950e40..3ae256b7 100644 --- a/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/icsdroid/db/LocalCalendar.kt @@ -93,46 +93,6 @@ class LocalCalendar private constructor( info.getAsLong(COLUMN_DEFAULT_ALARM)?.let { defaultAlarmMinutes = it } } - private fun updateAlarms() { - queryEvents(null, null) - .also { if (ignoreEmbeddedAlerts == true) Log.d(Constants.TAG, "Removing all alarms for ${it.size} events.") } - .filter { it.event != null } - .forEach { ev -> - val event = ev.event!! - - // according to setting: remove all alerts for every event - if (ignoreEmbeddedAlerts == true) { - Log.d(Constants.TAG, "Removing all alarms from ${event.uid}: $event") - event.alarms.clear() - } - - // according to setting: add default alarm for every event - defaultAlarmMinutes?.let { minutes -> - // Check if already added alarm - val alarm = event.alarms.find { it.description.value.contains("*added by ICSx5") } - if (alarm != null) return@let - // Add the default alarm to the event - Log.d(Constants.TAG, "Adding the default alarm to ${event.uid}.") - event.alarms.add( - // Create the new VAlarm - VAlarm.Factory().createComponent( - // Set all the properties for the alarm - PropertyList().apply { - // Set action to DISPLAY - add(Action.DISPLAY) - // Add the trigger x minutes before - val duration = Duration.ofMinutes(minutes * -1) - add(Trigger(duration)) - // Set a default description for checking if added - add(Description("*added by ICSx5")) - } - ) - ) - } - ev.update(event) - } - } - fun updateStatusSuccess(eTag: String?, lastModified: Long) { this.eTag = eTag this.lastModified = lastModified @@ -146,8 +106,6 @@ class LocalCalendar private constructor( values.put(COLUMN_DEFAULT_ALARM, defaultAlarmMinutes) values.put(COLUMN_IGNORE_EMBEDDED, ignoreEmbeddedAlerts) update(values) - - updateAlarms() } fun updateStatusNotModified() { @@ -156,8 +114,6 @@ class LocalCalendar private constructor( val values = ContentValues(1) values.put(COLUMN_LAST_SYNC, lastSync) update(values) - - updateAlarms() } fun updateStatusError(message: String) { From 77ea706c88cf508ec339edefe2040b30ab7cd257 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 8 Dec 2022 10:57:42 +0100 Subject: [PATCH 29/32] Cleanup Signed-off-by: Arnau Mora --- app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt index f739d7d0..a498efed 100644 --- a/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/icsdroid/SyncWorker.kt @@ -41,13 +41,11 @@ class SyncWorker( val request = OneTimeWorkRequestBuilder() .setInputData(workDataOf(FORCE_RESYNC to forceResync)) - val policy: ExistingWorkPolicy - if (force) { + val policy: ExistingWorkPolicy = if (force) { Log.i(Constants.TAG, "Manual sync, ignoring network condition") // overwrite existing syncs (which may have unwanted constraints) - policy = ExistingWorkPolicy.REPLACE - + ExistingWorkPolicy.REPLACE } else { // regular sync, requires network request.setConstraints(Constraints.Builder() @@ -55,7 +53,7 @@ class SyncWorker( .build()) // don't overwrite previous syncs (whether regular or manual) - policy = ExistingWorkPolicy.KEEP + ExistingWorkPolicy.KEEP } WorkManager.getInstance(context) From b88c537c110e2f400e2c775dc9fba8917724bcba Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 10 Dec 2022 14:43:29 +0100 Subject: [PATCH 30/32] Use Joda for time interval formatting; minor changes --- app/build.gradle | 1 + .../at/bitfire/icsdroid/ProcessEventsTask.kt | 4 +- .../icsdroid/ui/EditCalendarActivity.kt | 2 +- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 47 +++++-------------- app/src/main/res/values/strings.xml | 20 -------- 5 files changed, 16 insertions(+), 58 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5e32c3a9..1cc068d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}" implementation "com.squareup.okhttp3:okhttp-brotli:${versions.okhttp}" implementation "com.squareup.okhttp3:okhttp-coroutines:${versions.okhttp}" + implementation "joda-time:joda-time:2.12.1" // latest commons that don't require Java 8 //noinspection GradleDependency diff --git a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt index 97bc1acd..b46f50e0 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ProcessEventsTask.kt @@ -93,10 +93,8 @@ class ProcessEventsTask( // Set action to DISPLAY add(Action.DISPLAY) // Add the trigger x minutes before - val duration = Duration.ofMinutes(minutes * -1) + val duration = Duration.ofMinutes(-minutes) add(Trigger(duration)) - // Set a default description for checking if added - add(Description("*added by ICSx5")) } ) ) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt index 89893453..27e1e566 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/EditCalendarActivity.kt @@ -186,7 +186,7 @@ class EditCalendarActivity: AppCompatActivity() { var success = false model.calendar.value?.let { calendar -> try { - val values = ContentValues(4) + val values = ContentValues(5) values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, titleColorModel.title.value) values.put(CalendarContract.Calendars.CALENDAR_COLOR, titleColorModel.color.value) values.put(CalendarContract.Calendars.SYNC_EVENTS, if (model.active.value == true) 1 else 0) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index db867d82..17e44aec 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -17,21 +17,13 @@ import androidx.lifecycle.ViewModel import at.bitfire.icsdroid.R import at.bitfire.icsdroid.databinding.TitleColorBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlin.math.roundToInt - -const val MINUTES_IN_AN_HOUR = 60f -const val MINUTES_IN_A_DAY = 24*60f -const val MINUTES_IN_AN_WEEK = 7*24*60f -const val MINUTES_IN_A_MONTH = 30*7*24*60f +import org.joda.time.Minutes +import org.joda.time.format.PeriodFormat class TitleColorFragment : Fragment() { private val model by activityViewModels() - private val colorPickerContract = registerForActivityResult(ColorPickerActivity.Contract()) { color -> - model.color.postValue(color) - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, inState: Bundle?): View { val binding = TitleColorBinding.inflate(inflater, container, false) binding.lifecycleOwner = this @@ -44,30 +36,19 @@ class TitleColorFragment : Fragment() { firstCheck = false if (min == null) { binding.defaultAlarmText.visibility = View.GONE - return@observe - } - // TODO: Build string to have exactly the duration specified. e.g.: 1 hour and 37 minutes - val minutes = min.toInt() - val text = if (minutes < MINUTES_IN_AN_HOUR) - resources.getQuantityString(R.plurals.add_calendar_alarms_custom_minutes, minutes, minutes) - else if (minutes < MINUTES_IN_A_DAY) (minutes / MINUTES_IN_AN_HOUR).roundToInt().let { - resources.getQuantityString(R.plurals.add_calendar_alarms_custom_hours, it, it) - } - else if (minutes < MINUTES_IN_AN_WEEK) (minutes / MINUTES_IN_A_DAY).roundToInt().let { - resources.getQuantityString(R.plurals.add_calendar_alarms_custom_days, it, it) + } else { + val alarmPeriodText = PeriodFormat.wordBased().print(Minutes.minutes(min.toInt())) + binding.defaultAlarmText.text = getString(R.string.add_calendar_alarms_default_description, alarmPeriodText) + binding.defaultAlarmText.visibility = View.VISIBLE } - else if (minutes < MINUTES_IN_A_MONTH) (minutes / MINUTES_IN_AN_WEEK).roundToInt().let { - resources.getQuantityString(R.plurals.add_calendar_alarms_custom_weeks, it, it) - } - else (minutes / MINUTES_IN_A_MONTH).roundToInt().let { - resources.getQuantityString(R.plurals.add_calendar_alarms_custom_months, it, it) - } - binding.defaultAlarmText.text = getString(R.string.add_calendar_alarms_default_description, text) - binding.defaultAlarmText.visibility = View.VISIBLE } - // Listener for launching the color picker - binding.color.setOnClickListener { colorPickerContract.launch(model.color.value) } + val colorPickerContract = registerForActivityResult(ColorPickerActivity.Contract()) { color -> + model.color.postValue(color) + } + binding.color.setOnClickListener { + colorPickerContract.launch(model.color.value) + } binding.defaultAlarmSwitch.setOnCheckedChangeListener { _, checked -> if (firstCheck) return@setOnCheckedChangeListener @@ -94,9 +75,7 @@ class TitleColorFragment : Fragment() { .setMessage(R.string.default_alarm_dialog_message) .setView(editText) .setPositiveButton(R.string.default_alarm_dialog_set) { dialog, _ -> - if (editText.error != null) { - // TODO: Value introduced is not valid - } else { + if (editText.error == null) { model.defaultAlarmMinutes.postValue(editText.text?.toString()?.toLongOrNull()) dialog.dismiss() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70919890..abecaf1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,26 +47,6 @@ If enabled, all the incoming alarms from the server will be dismissed. Add a default alarm for all events Alarms set to %s before - - %d minute - %d minutes - - - %d hour - %d hours - - - %d day - %d days - - - %d week - %d days - - - %d month - %d months - Add default alarm This will add an alarm for all events Minutes before event From c233aeebd1339bafa36979c51602cc0357f1d2af Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sun, 11 Dec 2022 20:26:35 +0100 Subject: [PATCH 31/32] Removed `firstCheck` variable Signed-off-by: Arnau Mora --- .../bitfire/icsdroid/ui/TitleColorFragment.kt | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index 17e44aec..7ae41612 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -8,6 +8,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.CompoundButton.OnCheckedChangeListener import android.widget.EditText import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment @@ -24,16 +26,53 @@ class TitleColorFragment : Fragment() { private val model by activityViewModels() + private lateinit var binding: TitleColorBinding + + private val checkboxCheckedChanged: OnCheckedChangeListener = OnCheckedChangeListener { _, checked -> + if (!checked) { + model.defaultAlarmMinutes.postValue(null) + return@OnCheckedChangeListener + } + + val editText = EditText(requireContext()).apply { + setHint(R.string.default_alarm_dialog_hint) + + addTextChangedListener { txt -> + val text = txt?.toString() + val num = text?.toLongOrNull() + error = if (text == null || text.isBlank() || num == null) + getString(R.string.default_alarm_dialog_error) + else + null + } + } + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.default_alarm_dialog_title) + .setMessage(R.string.default_alarm_dialog_message) + .setView(editText) + .setPositiveButton(R.string.default_alarm_dialog_set) { dialog, _ -> + if (editText.error == null) { + model.defaultAlarmMinutes.postValue(editText.text?.toString()?.toLongOrNull()) + dialog.dismiss() + } + } + .setOnCancelListener { + binding.defaultAlarmSwitch.isChecked = false + } + .create() + .show() + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, inState: Bundle?): View { - val binding = TitleColorBinding.inflate(inflater, container, false) + binding = TitleColorBinding.inflate(inflater, container, false) binding.lifecycleOwner = this binding.model = model - var firstCheck = true - model.defaultAlarmMinutes.observe(viewLifecycleOwner) { min: Long? -> binding.defaultAlarmSwitch.isChecked = min != null - firstCheck = false + // We add the listener once the switch has an initial value + binding.defaultAlarmSwitch.setOnCheckedChangeListener(checkboxCheckedChanged) + if (min == null) { binding.defaultAlarmText.visibility = View.GONE } else { @@ -50,43 +89,6 @@ class TitleColorFragment : Fragment() { colorPickerContract.launch(model.color.value) } - binding.defaultAlarmSwitch.setOnCheckedChangeListener { _, checked -> - if (firstCheck) return@setOnCheckedChangeListener - - if (!checked) { - model.defaultAlarmMinutes.postValue(null) - return@setOnCheckedChangeListener - } - - val editText = EditText(requireContext()).apply { - setHint(R.string.default_alarm_dialog_hint) - - addTextChangedListener { txt -> - val text = txt?.toString() - val num = text?.toLongOrNull() - error = if (text == null || text.isBlank() || num == null) - getString(R.string.default_alarm_dialog_error) - else - null - } - } - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.default_alarm_dialog_title) - .setMessage(R.string.default_alarm_dialog_message) - .setView(editText) - .setPositiveButton(R.string.default_alarm_dialog_set) { dialog, _ -> - if (editText.error == null) { - model.defaultAlarmMinutes.postValue(editText.text?.toString()?.toLongOrNull()) - dialog.dismiss() - } - } - .setOnCancelListener { - binding.defaultAlarmSwitch.isChecked = false - } - .create() - .show() - } - return binding.root } From a68b1689071791606ecd30a0a94135cb6249a33c Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sun, 11 Dec 2022 20:30:38 +0100 Subject: [PATCH 32/32] Refactored check Signed-off-by: Arnau Mora --- .../java/at/bitfire/icsdroid/ui/TitleColorFragment.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt index 7ae41612..c6ab54a5 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/TitleColorFragment.kt @@ -107,12 +107,8 @@ class TitleColorFragment : Fragment() { var originalDefaultAlarmMinutes: Long? = null val defaultAlarmMinutes = MutableLiveData() - fun dirty() = arrayOf( - originalTitle to title, - originalColor to color, - originalIgnoreAlerts to ignoreAlerts, - originalDefaultAlarmMinutes to defaultAlarmMinutes, - ).any { (original, state) -> original != state.value } + fun dirty(): Boolean = originalTitle != title.value || originalColor != color.value || originalIgnoreAlerts != ignoreAlerts.value || + originalDefaultAlarmMinutes != defaultAlarmMinutes.value } }