diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeDialog.kt
new file mode 100644
index 0000000000..be32fe85a4
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeDialog.kt
@@ -0,0 +1,169 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2025 Infomaniak Network SA
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.alertDialogs
+
+import android.content.Context
+import android.text.format.DateFormat
+import androidx.annotation.StringRes
+import androidx.core.view.isVisible
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.TimeFormat
+import com.infomaniak.lib.core.utils.*
+import com.infomaniak.mail.R
+import com.infomaniak.mail.databinding.DialogSelectDateAndTimeBinding
+import com.infomaniak.mail.utils.date.DateFormatUtils.formatTime
+import java.util.Calendar
+import java.util.Date
+import java.util.TimeZone
+import com.infomaniak.lib.core.R as RCore
+
+abstract class SelectDateAndTimeDialog(private val activityContext: Context) : BaseAlertDialog(activityContext) {
+
+ @get:StringRes
+ abstract val positiveButtonText: Int
+
+ abstract fun defineCalendarConstraint(): CalendarConstraints.Builder
+
+ abstract fun getDelayTooShortErrorMessage(): String
+
+ val binding: DialogSelectDateAndTimeBinding by lazy { DialogSelectDateAndTimeBinding.inflate(activity.layoutInflater) }
+
+ override val alertDialog = initDialog()
+
+ private var onDateSelected: ((Long) -> Unit)? = null
+ private var onAbort: (() -> Unit)? = null
+
+ private lateinit var selectedDate: Date
+
+ private fun initDialog() = with(binding) {
+ MaterialAlertDialogBuilder(context)
+ .setView(root)
+ .setPositiveButton(R.string.buttonConfirm, null)
+ .setNegativeButton(RCore.string.buttonCancel, null)
+ .create()
+ }
+
+ final override fun resetCallbacks() {
+ onDateSelected = null
+ onAbort = null
+ }
+
+ fun show(onDateSelected: (Long) -> Unit, onAbort: (() -> Unit)? = null) {
+ showDialogWithBasicInfo()
+ setupListeners(onDateSelected, onAbort)
+ }
+
+ private fun showDialogWithBasicInfo() {
+ alertDialog.show()
+
+ selectDate(Date().roundUpToNextTenMinutes())
+ positiveButton.setText(positiveButtonText)
+ }
+
+ private fun setupListeners(onDateSelected: (Long) -> Unit, onAbort: (() -> Unit)?) = with(alertDialog) {
+
+ binding.dateField.setOnClickListener {
+ showDatePicker(selectedDate) { time ->
+ val date = Date(time).let { newDate ->
+ Calendar.getInstance().apply {
+ set(newDate.year(), newDate.month(), newDate.day(), selectedDate.hours(), selectedDate.minutes(), 0)
+ }.time
+ }
+
+ selectDate(date)
+ }
+ }
+
+ binding.timeField.setOnClickListener {
+ showTimePicker(selectedDate) { hour, minute ->
+ selectDate(selectedDate.setHour(hour).setMinute(minute))
+ }
+ }
+
+ this@SelectDateAndTimeDialog.onDateSelected = onDateSelected
+ this@SelectDateAndTimeDialog.onAbort = onAbort
+
+ positiveButton.setOnClickListener {
+ this@SelectDateAndTimeDialog.onDateSelected?.invoke(selectedDate.time)
+ dismiss()
+ }
+
+ negativeButton.setOnClickListener { cancel() }
+
+ setOnCancelListener { onAbort?.invoke() }
+ }
+
+ private fun selectDate(date: Date) {
+ updateErrorMessage(date)
+ selectedDate = date
+ with(binding) {
+ dateField.setText(date.format(FORMAT_DATE_DAY_MONTH_YEAR))
+ timeField.setText(context.formatTime(date))
+ }
+ }
+
+ private fun updateErrorMessage(date: Date) {
+ val isValid = date.isAtLeastXMinutesInTheFuture(MIN_SELECTABLE_DATE_MINUTES)
+
+ if (isValid.not()) binding.errorMessage.text = getErrorText(date)
+ binding.errorMessage.isVisible = isValid.not()
+ positiveButton.isEnabled = isValid
+ }
+
+ private fun getErrorText(date: Date): String = if (date.isInTheFuture()) {
+ getDelayTooShortErrorMessage()
+ } else {
+ activityContext.resources.getString(R.string.errorChooseUpcomingDate)
+ }
+
+ private fun showTimePicker(dateToDisplay: Date, onDateSelected: (Int, Int) -> Unit) {
+ val timePicker = MaterialTimePicker.Builder()
+ .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
+ .setTimeFormat(if (DateFormat.is24HourFormat(activityContext)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H)
+ .setHour(dateToDisplay.hours())
+ .setMinute(dateToDisplay.minutes())
+ .setTitleText(binding.context.getString(R.string.selectTimeDialogTitle))
+ .build()
+
+ timePicker.addOnPositiveButtonClickListener { onDateSelected(timePicker.hour, timePicker.minute) }
+
+ timePicker.show(super.activity.supportFragmentManager, null)
+ }
+
+ private fun showDatePicker(dateToDisplay: Date, onDateSelected: (Long) -> Unit) {
+ // MaterialDatePicker expects the `setSelection()` time to be defined as UTC time and not local time
+ val utcTime = dateToDisplay.time + TimeZone.getDefault().getOffset(dateToDisplay.time)
+
+ val datePicker = MaterialDatePicker.Builder.datePicker()
+ .setTitleText(binding.context.getString(R.string.selectDateDialogTitle))
+ .setSelection(utcTime)
+ .setCalendarConstraints(defineCalendarConstraint().build())
+ .build()
+
+ datePicker.addOnPositiveButtonClickListener(onDateSelected)
+
+ datePicker.show(super.activity.supportFragmentManager, null)
+ }
+
+ companion object {
+ const val MIN_SELECTABLE_DATE_MINUTES = 5
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt
index 458c08a1c0..b4e4cd18f1 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt
@@ -18,162 +18,39 @@
package com.infomaniak.mail.ui.alertDialogs
import android.content.Context
-import android.text.format.DateFormat
-import androidx.core.view.isVisible
-import com.google.android.material.datepicker.*
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.timepicker.MaterialTimePicker
-import com.google.android.material.timepicker.TimeFormat
-import com.infomaniak.lib.core.utils.*
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.CompositeDateValidator
+import com.google.android.material.datepicker.DateValidatorPointBackward
+import com.google.android.material.datepicker.DateValidatorPointForward
+import com.infomaniak.lib.core.utils.addYears
import com.infomaniak.mail.R
-import com.infomaniak.mail.databinding.DialogSelectDateAndTimeForScheduledDraftBinding
-import com.infomaniak.mail.utils.date.DateFormatUtils.formatTime
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
-import java.util.Calendar
import java.util.Date
-import java.util.TimeZone
import javax.inject.Inject
-import com.infomaniak.lib.core.R as RCore
@ActivityScoped
open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor(
@ActivityContext private val activityContext: Context,
-) : BaseAlertDialog(activityContext) {
+) : SelectDateAndTimeDialog(activityContext) {
- val binding: DialogSelectDateAndTimeForScheduledDraftBinding by lazy {
- DialogSelectDateAndTimeForScheduledDraftBinding.inflate(activity.layoutInflater)
- }
-
- override val alertDialog = initDialog()
-
- private var onSchedule: ((Long) -> Unit)? = null
- private var onAbort: (() -> Unit)? = null
-
- private lateinit var selectedDate: Date
-
- private fun initDialog() = with(binding) {
- MaterialAlertDialogBuilder(context)
- .setView(root)
- .setPositiveButton(R.string.buttonConfirm, null)
- .setNegativeButton(RCore.string.buttonCancel, null)
- .create()
- }
-
- final override fun resetCallbacks() {
- onSchedule = null
- onAbort = null
- }
-
- fun show(onSchedule: (Long) -> Unit, onAbort: (() -> Unit)? = null) {
- showDialogWithBasicInfo()
- setupListeners(onSchedule, onAbort)
- }
-
- private fun showDialogWithBasicInfo() {
- alertDialog.show()
-
- selectDate(Date().roundUpToNextTenMinutes())
- positiveButton.setText(R.string.buttonScheduleTitle)
- }
-
- private fun setupListeners(onSchedule: (Long) -> Unit, onAbort: (() -> Unit)?) = with(alertDialog) {
-
- binding.dateField.setOnClickListener {
- showDatePicker(selectedDate) { time ->
- val date = Date(time).let { newDate ->
- Calendar.getInstance().apply {
- set(newDate.year(), newDate.month(), newDate.day(), selectedDate.hours(), selectedDate.minutes(), 0)
- }.time
- }
-
- selectDate(date)
- }
- }
-
- binding.timeField.setOnClickListener {
- showTimePicker(selectedDate) { hour, minute ->
- selectDate(selectedDate.setHour(hour).setMinute(minute))
- }
- }
-
- this@SelectDateAndTimeForScheduledDraftDialog.onSchedule = onSchedule
- this@SelectDateAndTimeForScheduledDraftDialog.onAbort = onAbort
-
- positiveButton.setOnClickListener {
- this@SelectDateAndTimeForScheduledDraftDialog.onSchedule?.invoke(selectedDate.time)
- dismiss()
- }
-
- negativeButton.setOnClickListener { cancel() }
+ override val positiveButtonText: Int = R.string.buttonScheduleTitle
- setOnCancelListener { onAbort?.invoke() }
- }
-
- private fun selectDate(date: Date) {
- updateErrorMessage(date)
- selectedDate = date
- with(binding) {
- dateField.setText(date.format(FORMAT_DATE_DAY_MONTH_YEAR))
- timeField.setText(context.formatTime(date))
- }
- }
-
- private fun updateErrorMessage(date: Date) {
- val isValid = date.isAtLeastXMinutesInTheFuture(MIN_SCHEDULE_DELAY_MINUTES)
-
- if (isValid.not()) binding.scheduleDateError.text = getScheduleDateErrorText(date)
- binding.scheduleDateError.isVisible = isValid.not()
- positiveButton.isEnabled = isValid
- }
-
- private fun getScheduleDateErrorText(date: Date): String = if (date.isInTheFuture()) {
- activityContext.resources.getQuantityString(
- R.plurals.errorScheduleDelayTooShort,
- MIN_SCHEDULE_DELAY_MINUTES,
- MIN_SCHEDULE_DELAY_MINUTES,
- )
- } else {
- activityContext.resources.getString(R.string.errorChooseUpcomingDate)
- }
-
- private fun showTimePicker(dateToDisplay: Date, onDateSelected: (Int, Int) -> Unit) {
- val timePicker = MaterialTimePicker.Builder()
- .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
- .setTimeFormat(if (DateFormat.is24HourFormat(activityContext)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H)
- .setHour(dateToDisplay.hours())
- .setMinute(dateToDisplay.minutes())
- .setTitleText(binding.context.getString(R.string.selectTimeDialogTitle))
- .build()
-
- timePicker.addOnPositiveButtonClickListener { onDateSelected(timePicker.hour, timePicker.minute) }
-
- timePicker.show(super.activity.supportFragmentManager, null)
- }
-
- private fun showDatePicker(dateToDisplay: Date, onDateSelected: (Long) -> Unit) {
+ override fun defineCalendarConstraint(): CalendarConstraints.Builder {
val dateValidators = listOf(
DateValidatorPointForward.now(),
DateValidatorPointBackward.before(Date().addYears(MAX_SCHEDULE_DELAY_YEARS).time),
)
- val constraintsBuilder = CalendarConstraints.Builder().setValidator(CompositeDateValidator.allOf(dateValidators))
-
- // MaterialDatePicker expects the `setSelection()` time to be defined as UTC time and not local time
- val utcTime = dateToDisplay.time + TimeZone.getDefault().getOffset(dateToDisplay.time)
-
- val datePicker = MaterialDatePicker.Builder.datePicker()
- .setTitleText(binding.context.getString(R.string.selectDateDialogTitle))
- .setSelection(utcTime)
- .setCalendarConstraints(constraintsBuilder.build())
- .build()
-
- datePicker.addOnPositiveButtonClickListener(onDateSelected)
-
- datePicker.show(super.activity.supportFragmentManager, null)
+ return CalendarConstraints.Builder().setValidator(CompositeDateValidator.allOf(dateValidators))
}
+ override fun getDelayTooShortErrorMessage(): String = activityContext.resources.getQuantityString(
+ R.plurals.errorScheduleDelayTooShort,
+ MIN_SELECTABLE_DATE_MINUTES,
+ MIN_SELECTABLE_DATE_MINUTES,
+ )
+
companion object {
- const val MIN_SCHEDULE_DELAY_MINUTES = 5
const val MAX_SCHEDULE_DELAY_YEARS = 10
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt
index 41bb05c3d8..014fa4d924 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt
@@ -31,7 +31,7 @@ import com.infomaniak.lib.core.utils.*
import com.infomaniak.mail.MatomoMail.trackScheduleSendEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.databinding.BottomSheetScheduleSendBinding
-import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog.Companion.MIN_SCHEDULE_DELAY_MINUTES
+import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeDialog.Companion.MIN_SELECTABLE_DATE_MINUTES
import com.infomaniak.mail.ui.main.thread.actions.ActionItemView
import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithoutYear
import dagger.hilt.android.AndroidEntryPoint
@@ -72,7 +72,7 @@ class ScheduleSendBottomSheetDialog @Inject constructor() : BottomSheetDialogFra
}
private fun computeLastScheduleItem() = with(binding) {
- if (Date(lastSelectedScheduleEpoch).isAtLeastXMinutesInTheFuture(MIN_SCHEDULE_DELAY_MINUTES)) {
+ if (Date(lastSelectedScheduleEpoch).isAtLeastXMinutesInTheFuture(MIN_SELECTABLE_DATE_MINUTES)) {
lastScheduleItem.isVisible = true
lastScheduleItem.setDescription(context.dayOfWeekDateWithoutYear(date = Date(lastSelectedScheduleEpoch)))
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt
index acf211acf6..951c6fb488 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt
@@ -539,7 +539,7 @@ class ThreadFragment : Fragment() {
getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean ->
dateAndTimeScheduleDialog.show(
- onSchedule = { timestamp ->
+ onDateSelected = { timestamp ->
localSettings.lastSelectedScheduleEpoch = timestamp
mainViewModel.rescheduleDraft(Date(timestamp))
},
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt
index b5af49e41a..f113c74024 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt
@@ -224,7 +224,7 @@ class NewMessageFragment : Fragment() {
getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean ->
dateAndTimeScheduleDialog.show(
- onSchedule = { timestamp ->
+ onDateSelected = { timestamp ->
localSettings.lastSelectedScheduleEpoch = timestamp
scheduleDraft(timestamp)
},
diff --git a/app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml b/app/src/main/res/layout/dialog_select_date_and_time.xml
similarity index 98%
rename from app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml
rename to app/src/main/res/layout/dialog_select_date_and_time.xml
index 5e16788f60..a746bb5c8e 100644
--- a/app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml
+++ b/app/src/main/res/layout/dialog_select_date_and_time.xml
@@ -74,7 +74,7 @@