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

@ -11,6 +11,7 @@
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<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.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@ -242,6 +243,18 @@
android:name=".receivers.ScheduledMessageReceiver"
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
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View file

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

View file

@ -11,7 +11,6 @@ import android.os.Bundle
import android.provider.Telephony
import android.text.TextUtils
import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import org.fossify.commons.dialogs.PermissionRequiredDialog
import org.fossify.commons.extensions.adjustAlpha
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.Companion.ADDRESS_SEPARATOR
import org.fossify.messages.messaging.SmsSender
import org.fossify.messages.messaging.scheduleMessage
import org.fossify.messages.models.Attachment
import org.fossify.messages.models.Conversation
import org.fossify.messages.models.Draft
@ -77,6 +78,7 @@ import org.fossify.messages.models.NamePhoto
import org.fossify.messages.models.RecycleBinMessage
import org.xmlpull.v1.XmlPullParserException
import java.io.FileNotFoundException
import kotlin.time.Duration.Companion.minutes
val Context.config: Config
get() = Config.newInstance(applicationContext)
@ -1311,13 +1313,13 @@ fun Context.updateScheduledMessagesThreadId(messages: List<Message>, newThreadId
fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List<Message>? = null) {
val messages = messagesToDelete ?: messagesDB.getScheduledThreadMessages(threadId)
val now = System.currentTimeMillis() + 500L
val cutoff = System.currentTimeMillis() - 1.minutes.inWholeMilliseconds
try {
messages.filter { it.isScheduled && it.millis() < now }.forEach { msg ->
messages.filter { it.isScheduled && it.millis() < cutoff }.forEach { msg ->
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
val conversation = conversationsDB.getConversationWithThreadId(threadId)
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 {
return resources.getDimensionPixelSize(R.dimen.default_keyboard_height)
}

View file

@ -1,6 +1,11 @@
@file:Suppress("MaxLineLength")
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.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")
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")
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.refreshMessages
import org.fossify.messages.messaging.sendMessageCompat
import kotlin.time.Duration.Companion.minutes
class ScheduledMessageReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val wakelock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "simple.messenger:scheduled.message.receiver")
wakelock.acquire(3000)
val wakelock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"simple.messenger:scheduled.message.receiver"
)
wakelock.acquire(1.minutes.inWholeMilliseconds)
val pendingResult = goAsync()
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) {
context.showErrorToast(e)
} 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)
)
}
}
}