fix: reschedule scheduled messages when they are cleared (#654)

* fix: reschedule scheduled messages when they are cleared

* docs: remove comment about overdue messages

That will be solved in another PR.

* fix: address detekt issues

* fix: don't clear scheduled message ahead of time

* fix: reschedule scheduled messages on startup

This recovers the alarms when app was force-stopped.

* fix: typo!

* fix: another typo!

Refs: https://github.com/FossifyOrg/Messages/issues/641
This commit is contained in:
Naveen Singh 2026-01-23 12:00:04 +05:30 committed by GitHub
parent dd4ff67a72
commit a8eb1956b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 89 additions and 11 deletions

View file

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed missing notifications in some cases ([#159]) - Fixed missing notifications in some cases ([#159])
- Fixed incorrect blocking of MMS messages in some rare cases ([#644]) - Fixed incorrect blocking of MMS messages in some rare cases ([#644])
- Fixed issue with importing alphanumeric blocked numbers ([#282]) - Fixed issue with importing alphanumeric blocked numbers ([#282])
- Fixed issue where scheduled messages were not sent after a reboot or app updates ([#641])
## [1.7.0] - 2025-12-16 ## [1.7.0] - 2025-12-16
### Added ### Added
@ -206,6 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#209]: https://github.com/FossifyOrg/Messages/issues/209 [#209]: https://github.com/FossifyOrg/Messages/issues/209
[#217]: https://github.com/FossifyOrg/Messages/issues/217 [#217]: https://github.com/FossifyOrg/Messages/issues/217
[#225]: https://github.com/FossifyOrg/Messages/issues/225 [#225]: https://github.com/FossifyOrg/Messages/issues/225
[#232]: https://github.com/FossifyOrg/Messages/issues/232
[#234]: https://github.com/FossifyOrg/Messages/issues/234 [#234]: https://github.com/FossifyOrg/Messages/issues/234
[#243]: https://github.com/FossifyOrg/Messages/issues/243 [#243]: https://github.com/FossifyOrg/Messages/issues/243
[#262]: https://github.com/FossifyOrg/Messages/issues/262 [#262]: https://github.com/FossifyOrg/Messages/issues/262
@ -231,6 +233,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#574]: https://github.com/FossifyOrg/Messages/issues/574 [#574]: https://github.com/FossifyOrg/Messages/issues/574
[#600]: https://github.com/FossifyOrg/Messages/issues/600 [#600]: https://github.com/FossifyOrg/Messages/issues/600
[#610]: https://github.com/FossifyOrg/Messages/issues/610 [#610]: https://github.com/FossifyOrg/Messages/issues/610
[#641]: https://github.com/FossifyOrg/Messages/issues/641
[#644]: https://github.com/FossifyOrg/Messages/issues/644 [#644]: https://github.com/FossifyOrg/Messages/issues/644
[#651]: https://github.com/FossifyOrg/Messages/issues/651 [#651]: https://github.com/FossifyOrg/Messages/issues/651

View file

@ -11,6 +11,7 @@
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" /> <uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@ -242,6 +243,18 @@
android:name=".receivers.ScheduledMessageReceiver" android:name=".receivers.ScheduledMessageReceiver"
android:exported="false" /> android:exported="false" />
<receiver
android:name=".receivers.RescheduleAlarmsReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.TIME_SET" />
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
</intent-filter>
</receiver>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View file

@ -8,6 +8,8 @@ import android.provider.ContactsContract
import org.fossify.commons.FossifyApp import org.fossify.commons.FossifyApp
import org.fossify.commons.extensions.hasPermission import org.fossify.commons.extensions.hasPermission
import org.fossify.commons.helpers.PERMISSION_READ_CONTACTS import org.fossify.commons.helpers.PERMISSION_READ_CONTACTS
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.extensions.rescheduleAllScheduledMessages
import org.fossify.messages.helpers.MessagingCache import org.fossify.messages.helpers.MessagingCache
class App : FossifyApp() { class App : FossifyApp() {
@ -23,10 +25,14 @@ class App : FossifyApp() {
).forEach { ).forEach {
try { try {
contentResolver.registerContentObserver(it, true, contactsObserver) contentResolver.registerContentObserver(it, true, contactsObserver)
} catch (_: Exception){ } catch (_: Exception) {
} }
} }
} }
ensureBackgroundThread {
rescheduleAllScheduledMessages()
}
} }
private val contactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { private val contactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {

View file

@ -11,7 +11,6 @@ import android.os.Bundle
import android.provider.Telephony import android.provider.Telephony
import android.text.TextUtils import android.text.TextUtils
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import org.fossify.commons.dialogs.PermissionRequiredDialog import org.fossify.commons.dialogs.PermissionRequiredDialog
import org.fossify.commons.extensions.adjustAlpha import org.fossify.commons.extensions.adjustAlpha
import org.fossify.commons.extensions.appLaunched import org.fossify.commons.extensions.appLaunched

View file

@ -68,6 +68,7 @@ import org.fossify.messages.interfaces.MessagesDao
import org.fossify.messages.messaging.MessagingUtils import org.fossify.messages.messaging.MessagingUtils
import org.fossify.messages.messaging.MessagingUtils.Companion.ADDRESS_SEPARATOR import org.fossify.messages.messaging.MessagingUtils.Companion.ADDRESS_SEPARATOR
import org.fossify.messages.messaging.SmsSender import org.fossify.messages.messaging.SmsSender
import org.fossify.messages.messaging.scheduleMessage
import org.fossify.messages.models.Attachment import org.fossify.messages.models.Attachment
import org.fossify.messages.models.Conversation import org.fossify.messages.models.Conversation
import org.fossify.messages.models.Draft import org.fossify.messages.models.Draft
@ -77,6 +78,7 @@ import org.fossify.messages.models.NamePhoto
import org.fossify.messages.models.RecycleBinMessage import org.fossify.messages.models.RecycleBinMessage
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import java.io.FileNotFoundException import java.io.FileNotFoundException
import kotlin.time.Duration.Companion.minutes
val Context.config: Config val Context.config: Config
get() = Config.newInstance(applicationContext) get() = Config.newInstance(applicationContext)
@ -1311,13 +1313,13 @@ fun Context.updateScheduledMessagesThreadId(messages: List<Message>, newThreadId
fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List<Message>? = null) { fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List<Message>? = null) {
val messages = messagesToDelete ?: messagesDB.getScheduledThreadMessages(threadId) val messages = messagesToDelete ?: messagesDB.getScheduledThreadMessages(threadId)
val now = System.currentTimeMillis() + 500L val cutoff = System.currentTimeMillis() - 1.minutes.inWholeMilliseconds
try { try {
messages.filter { it.isScheduled && it.millis() < now }.forEach { msg -> messages.filter { it.isScheduled && it.millis() < cutoff }.forEach { msg ->
messagesDB.delete(msg.id) messagesDB.delete(msg.id)
} }
if (messages.filterNot { it.isScheduled && it.millis() < now }.isEmpty()) { if (messages.filterNot { it.isScheduled && it.millis() < cutoff }.isEmpty()) {
// delete empty temporary thread // delete empty temporary thread
val conversation = conversationsDB.getConversationWithThreadId(threadId) val conversation = conversationsDB.getConversationWithThreadId(threadId)
if (conversation != null && conversation.isScheduled) { if (conversation != null && conversation.isScheduled) {
@ -1330,6 +1332,18 @@ fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List
} }
} }
fun Context.rescheduleAllScheduledMessages() {
val scheduledMessages = try {
messagesDB.getAllScheduledMessages()
} catch (_: Exception) {
return
}
scheduledMessages.forEach { message ->
runCatching { scheduleMessage(message) }
}
}
fun Context.getDefaultKeyboardHeight(): Int { fun Context.getDefaultKeyboardHeight(): Int {
return resources.getDimensionPixelSize(R.dimen.default_keyboard_height) return resources.getDimensionPixelSize(R.dimen.default_keyboard_height)
} }

View file

@ -1,6 +1,11 @@
@file:Suppress("MaxLineLength")
package org.fossify.messages.interfaces package org.fossify.messages.interfaces
import androidx.room.* import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import org.fossify.messages.models.Message import org.fossify.messages.models.Message
import org.fossify.messages.models.RecycleBinMessage import org.fossify.messages.models.RecycleBinMessage
@ -24,6 +29,9 @@ interface MessagesDao {
@Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL") @Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL")
fun getAllRecycleBinMessages(): List<Message> fun getAllRecycleBinMessages(): List<Message>
@Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND is_scheduled = 1")
fun getAllScheduledMessages(): List<Message>
@Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND recycle_bin_messages.deleted_ts < :timestamp") @Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND recycle_bin_messages.deleted_ts < :timestamp")
fun getOldRecycleBinMessages(timestamp: Long): List<Message> fun getOldRecycleBinMessages(timestamp: Long): List<Message>

View file

@ -0,0 +1,20 @@
package org.fossify.messages.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.extensions.rescheduleAllScheduledMessages
/**
* Reschedules alarms after boot/package updates.
*/
class RescheduleAlarmsReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val pendingResult = goAsync()
ensureBackgroundThread {
context.rescheduleAllScheduledMessages()
pendingResult.finish()
}
}
}

View file

@ -17,17 +17,30 @@ import org.fossify.messages.helpers.THREAD_ID
import org.fossify.messages.helpers.refreshConversations import org.fossify.messages.helpers.refreshConversations
import org.fossify.messages.helpers.refreshMessages import org.fossify.messages.helpers.refreshMessages
import org.fossify.messages.messaging.sendMessageCompat import org.fossify.messages.messaging.sendMessageCompat
import kotlin.time.Duration.Companion.minutes
class ScheduledMessageReceiver : BroadcastReceiver() { class ScheduledMessageReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val wakelock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "simple.messenger:scheduled.message.receiver") val wakelock = powerManager.newWakeLock(
wakelock.acquire(3000) PowerManager.PARTIAL_WAKE_LOCK,
"simple.messenger:scheduled.message.receiver"
)
wakelock.acquire(1.minutes.inWholeMilliseconds)
val pendingResult = goAsync()
ensureBackgroundThread { ensureBackgroundThread {
handleIntent(context, intent) try {
handleIntent(context, intent)
} finally {
try {
if (wakelock.isHeld) wakelock.release()
} catch (_: Exception) {
}
pendingResult.finish()
}
} }
} }
@ -57,7 +70,9 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
} catch (e: Exception) { } catch (e: Exception) {
context.showErrorToast(e) context.showErrorToast(e)
} catch (e: Error) { } catch (e: Error) {
context.showErrorToast(e.localizedMessage ?: context.getString(org.fossify.commons.R.string.unknown_error_occurred)) context.showErrorToast(
e.localizedMessage ?: context.getString(org.fossify.commons.R.string.unknown_error_occurred)
)
} }
} }
} }