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 @@