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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ SMS Gateway turns your Android smartphone into an SMS gateway. It's a lightweigh
- 💳 **Multiple SIM card support:** Supports devices with [multiple SIM cards](https://docs.sms-gate.app/features/multi-sim/).
- 📱📱 **Multiple device support:** Connect [multiple devices](https://docs.sms-gate.app/features/multi-device/) to the same account with Cloud or Private server. Messages sent via the server are distributed across all connected devices.
- 💾 **Data SMS support:** Send and receive binary [data payloads](https://docs.sms-gate.app/features/data-sms.md) via SMS for IoT commands, encrypted messages, and other specialized use cases.
- 🕐 **Working hours scheduling:** Restrict message delivery to configurable time windows, automatically pausing the queue outside of them

🔌 Integration:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,27 @@ class MessagesService(
throw ConflictException()
}

if (scheduleAt != null
&& scheduleAt > System.currentTimeMillis()
&& scheduleAt < (nextScheduled ?: 0)
) {
SendMessagesWorker.start(context, true, scheduleAt)
val workHoursStart = if (priority < Message.PRIORITY_EXPEDITED) {
settings.nextWorkHoursStart()
} else {
null
}
val startTime = when {
workHoursStart != null -> workHoursStart
scheduleAt != null
&& scheduleAt > System.currentTimeMillis()
&& scheduleAt < (nextScheduled ?: 0) -> scheduleAt
else -> SendMessagesWorker.IMMEDIATE
}

if (startTime == SendMessagesWorker.IMMEDIATE) {
SendMessagesWorker.start(
context,
nextScheduled == null || priority >= Message.PRIORITY_EXPEDITED,
SendMessagesWorker.IMMEDIATE
)
} else {
SendMessagesWorker.start(context, true, startTime)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

return message
Expand Down Expand Up @@ -254,7 +264,7 @@ class MessagesService(

val priority = message.params.priority ?: Message.PRIORITY_DEFAULT

// apply limits if:
// apply rate limit if:
// - message is not expedited
// - message is expedited and previous message had higher or equal priority
if (priority < Message.PRIORITY_EXPEDITED
Expand All @@ -263,6 +273,11 @@ class MessagesService(
applyLimit()
}

// skip work hours for expedited messages
if (priority < Message.PRIORITY_EXPEDITED) {
applyWorkHoursLimit()
}

if (!withContext(NonCancellable) { sendMessage(message) }) {
// if message was not sent - don't need any delay before next message
continue
Expand Down Expand Up @@ -305,6 +320,16 @@ class MessagesService(
delay(settings.limitPeriod.duration - (System.currentTimeMillis() - processedStats.lastTimestamp) + 1000L)
}

private suspend fun applyWorkHoursLimit() {
val nextStart = settings.nextWorkHoursStart() ?: return

val delayMs = nextStart - System.currentTimeMillis()
if (delayMs > 0) {
SendMessagesWorker.start(context, true, nextStart)
delay(delayMs)
}
}

/**
* @return `true` if message was sent
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,76 @@ class MessagesSettings(
val processingOrder: ProcessingOrder
get() = storage.get<ProcessingOrder>(PROCESSING_ORDER) ?: ProcessingOrder.LIFO

val workHoursEnabled: Boolean
get() = storage.get<Boolean>(WORK_HOURS_ENABLED) ?: false
val workHoursStart: String
get() = storage.get(WORK_HOURS_START) ?: "09:00"
val workHoursEnd: String
get() = storage.get(WORK_HOURS_END) ?: "19:00"

private fun isInWorkHours(): Boolean {
if (!workHoursEnabled) return true

val now = java.util.Calendar.getInstance()
val nowMinutes =
now.get(java.util.Calendar.HOUR_OF_DAY) * 60 + now.get(java.util.Calendar.MINUTE)
val startMinutes = parseTimeMinutes(workHoursStart)
val endMinutes = parseTimeMinutes(workHoursEnd)

if (startMinutes == endMinutes) return true

return if (startMinutes < endMinutes) {
nowMinutes in startMinutes until endMinutes
} else {
nowMinutes >= startMinutes || nowMinutes < endMinutes
}
}

fun nextWorkHoursStart(): Long? {
if (!workHoursEnabled) return null
if (isInWorkHours()) return null

val now = java.util.Calendar.getInstance()
val nowMinutes =
now.get(java.util.Calendar.HOUR_OF_DAY) * 60 + now.get(java.util.Calendar.MINUTE)
val startMinutes = parseTimeMinutes(workHoursStart)
val endMinutes = parseTimeMinutes(workHoursEnd)

val todayStart = java.util.Calendar.getInstance().apply {
set(java.util.Calendar.HOUR_OF_DAY, startMinutes / 60)
set(java.util.Calendar.MINUTE, startMinutes % 60)
set(java.util.Calendar.SECOND, 0)
set(java.util.Calendar.MILLISECOND, 0)
}

return if (startMinutes <= endMinutes) {
if (nowMinutes < startMinutes) {
todayStart.timeInMillis
} else {
todayStart.add(java.util.Calendar.DAY_OF_MONTH, 1)
todayStart.timeInMillis
}
} else {
todayStart.timeInMillis
}
}

private fun parseTimeMinutes(value: String): Int {
val parts = value.split(":")
return parts.getOrNull(0)?.toIntOrNull()?.let { h ->
h * 60 + (parts.getOrNull(1)?.toIntOrNull() ?: 0)
} ?: 0
}

private fun isValidTimeFormat(value: String): Boolean {
val regex = Regex("^\\d{2}:\\d{2}$")
if (!regex.matches(value)) return false
val parts = value.split(":")
val h = parts.getOrNull(0)?.toIntOrNull() ?: return false
val m = parts.getOrNull(1)?.toIntOrNull() ?: return false
return h in 0..23 && m in 0..59
}

init {
migrate()
}
Expand Down Expand Up @@ -94,6 +164,10 @@ class MessagesSettings(
private const val LOG_LIFETIME_DAYS = "log_lifetime_days"

private const val PROCESSING_ORDER = "processing_order"

private const val WORK_HOURS_ENABLED = "work_hours_enabled"
private const val WORK_HOURS_START = "work_hours_start"
private const val WORK_HOURS_END = "work_hours_end"
}

override fun export(): Map<String, *> {
Expand All @@ -105,6 +179,9 @@ class MessagesSettings(
SIM_SELECTION_MODE to simSelectionMode.name,
LOG_LIFETIME_DAYS to logLifetimeDays,
PROCESSING_ORDER to processingOrder.name,
WORK_HOURS_ENABLED to workHoursEnabled,
WORK_HOURS_START to workHoursStart,
WORK_HOURS_END to workHoursEnd,
)
}

Expand Down Expand Up @@ -192,6 +269,41 @@ class MessagesSettings(
changed
}

WORK_HOURS_ENABLED -> {
val newValue = value?.toString()?.toBooleanStrictOrNull() ?: false
val changed = this.workHoursEnabled != newValue

storage.set(key, newValue.toString())

changed
}

WORK_HOURS_START -> {
val raw = value?.toString()
val newValue = if (raw != null && isValidTimeFormat(raw)) raw else "09:00"
if (raw != null && !isValidTimeFormat(raw)) {
throw IllegalArgumentException("Invalid time format: $raw")
}
val changed = this.workHoursStart != newValue

storage.set(key, newValue)

changed
}

WORK_HOURS_END -> {
val raw = value?.toString()
val newValue = if (raw != null && isValidTimeFormat(raw)) raw else "19:00"
if (raw != null && !isValidTimeFormat(raw)) {
throw IllegalArgumentException("Invalid time format: $raw")
}
val changed = this.workHoursEnd != newValue

storage.set(key, newValue)

changed
}
Comment thread
capcom6 marked this conversation as resolved.

else -> false
}
}.any { it }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.capcom.smsgateway.ui.settings

import android.app.TimePickerDialog
import android.content.SharedPreferences
import android.os.Bundle
import android.text.InputType
Expand Down Expand Up @@ -45,6 +46,25 @@ class MessagesSettingsFragment : BasePreferenceFragment() {
it.selectAll()
}
}

"messages.work_hours_start", "messages.work_hours_end" -> {
val pref = preference as EditTextPreference
val currentText = pref.text ?: "09:00"
val parts = currentText.split(":")
val hour = parts.getOrNull(0)?.toIntOrNull()?.coerceIn(0, 23) ?: 9
val minute = parts.getOrNull(1)?.toIntOrNull()?.coerceIn(0, 59) ?: 0

TimePickerDialog(
requireContext(),
{ _, h, m ->
pref.text = String.format(java.util.Locale.US, "%02d:%02d", h, m)
},
hour,
minute,
true
).show()
return
}
}

super.onDisplayPreferenceDialog(preference)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,9 @@
<string name="language">Language</string>
<string name="language_system">System default</string>
<string name="password_must_not_be_empty">Password must not be empty</string>
<string name="working_hours">Working Hours</string>
<string name="working_hours_enable">Enable working hours</string>
<string name="working_hours_summary">Only send messages within the defined time window</string>
<string name="working_hours_start">Start time</string>
<string name="working_hours_end">End time</string>
</resources>
23 changes: 23 additions & 0 deletions app/src/main/res/xml/messages_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,27 @@
app:title="@string/messages_count"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>

<PreferenceCategory app:title="@string/working_hours">
<SwitchPreference
android:defaultValue="false"
android:key="messages.work_hours_enabled"
app:icon="@drawable/ic_timer"
app:title="@string/working_hours_enable"
app:summary="@string/working_hours_summary" />
<EditTextPreference
android:defaultValue="09:00"
android:key="messages.work_hours_start"
app:icon="@drawable/ic_timer"
app:title="@string/working_hours_start"
app:useSimpleSummaryProvider="true"
android:dependency="messages.work_hours_enabled" />
<EditTextPreference
android:defaultValue="19:00"
android:key="messages.work_hours_end"
app:icon="@drawable/ic_timer"
app:title="@string/working_hours_end"
app:useSimpleSummaryProvider="true"
android:dependency="messages.work_hours_enabled" />
</PreferenceCategory>
</PreferenceScreen>
Loading